Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a84a2b3152 | ||
|
|
087dbefec2 | ||
|
|
46ba793521 | ||
|
|
21ab9bf109 | ||
|
|
05c8b53444 | ||
|
|
e7495e311b | ||
|
|
9c10ecf8ea | ||
|
|
9fa73aca86 | ||
|
|
8e7ce1216c | ||
|
|
4ddbdde553 | ||
|
|
cd24067f17 | ||
|
|
040adb2ddf | ||
|
|
4d578d2cdf | ||
|
|
1cf84a270c | ||
|
|
d03651312e | ||
|
|
4b34aec13d | ||
|
|
7ab8fdbcb4 | ||
|
|
b0bf87f3ed | ||
|
|
1490642260 | ||
|
|
04b49acbb0 | ||
|
|
ff79e5c70e | ||
|
|
5d51c79739 | ||
|
|
8bc2c08288 | ||
|
|
ade144aa20 | ||
|
|
fcd06c80e3 | ||
|
|
57bbba3d95 | ||
|
|
d952e77b78 | ||
|
|
73989da3b7 | ||
|
|
a534d39351 | ||
|
|
c16ef798f5 | ||
|
|
58cac84f4a | ||
|
|
1404581f78 | ||
|
|
ff18b9a733 | ||
|
|
82e52a1948 | ||
|
|
e3abd35ba7 | ||
|
|
dfdb04acf4 | ||
|
|
352a6ba8cf | ||
|
|
997d6a75e8 | ||
|
|
757e3a2faf |
+3
-1
@@ -147,6 +147,8 @@ NEXT_PUBLIC_SENDGRID_SENDER_NAME=
|
||||
# Used for capturing exceptions and logging messages
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
SENTRY_DEBUG=
|
||||
SENTRY_DISABLE_CLIENT_SOURCE_MAPS=
|
||||
SENTRY_DISABLE_SERVER_SOURCE_MAPS=
|
||||
SENTRY_MAX_SPANS=
|
||||
SENTRY_TRACES_SAMPLE_RATE=
|
||||
|
||||
@@ -414,4 +416,4 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
NEXT_PUBLIC_LOGGER_LEVEL=
|
||||
|
||||
# Used to use Replexica SDK, a tool for real-time AI-powered localization
|
||||
REPLEXICA_API_KEY=
|
||||
REPLEXICA_API_KEY=
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Draft release
|
||||
run-name: Draft release ${{ inputs.next_version }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
next_version:
|
||||
required: true
|
||||
type: string
|
||||
description: 'Version name'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
draft_release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --local user.email "github-actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Bump version
|
||||
run: |
|
||||
cd apps/web
|
||||
yarn version ${{ inputs.next_version }}
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "chore: release ${{ inputs.next_version }}"
|
||||
git push
|
||||
|
||||
- name: Draft release
|
||||
run: gh release create $VERSION --generate-notes --draft
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ inputs.next_version }}
|
||||
@@ -0,0 +1,21 @@
|
||||
name: Changes Requested
|
||||
on:
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
changes-requested:
|
||||
if: github.event.review.state == 'changes_requested'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Explanation
|
||||
run: echo "This workflow triggers a workflow_run; it's necessary because otherwise the repo secrets aren't available for 'pull_request_review' events from externally forked pull requests"
|
||||
- name: Save PR number to context.json
|
||||
run: |
|
||||
printf '{
|
||||
"pr_number": ${{ github.event.pull_request.number }}
|
||||
}' >> context.json
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: context.json
|
||||
path: ./
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Post release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: 'main'
|
||||
ssh-key: ${{ secrets.DEPLOY_KEY }}
|
||||
|
||||
- name: Configure git
|
||||
run: |
|
||||
git config --local user.email "github-actions@github.com"
|
||||
git config --local user.name "GitHub Actions"
|
||||
|
||||
- run: git push origin +main:production
|
||||
@@ -0,0 +1,104 @@
|
||||
name: Re-draft
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Changes Requested
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
download-context:
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Download artifact'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == "context.json"
|
||||
})[0];
|
||||
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data));
|
||||
|
||||
- name: 'Unzip artifact'
|
||||
run: unzip context.zip
|
||||
|
||||
- name: 'Return Parsed JSON'
|
||||
uses: actions/github-script@v7
|
||||
id: return-parsed-json
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
let data = fs.readFileSync('./context.json');
|
||||
return JSON.parse(data);
|
||||
|
||||
outputs:
|
||||
pr_number: ${{fromJSON(steps.return-parsed-json.outputs.result).pr_number}}
|
||||
|
||||
re-draft:
|
||||
needs:
|
||||
- download-context
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "This PR was rejected"
|
||||
- name: Convert PR to draft when changes are requested
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
async function getPullRequestId() {
|
||||
const pull_number = ${{ needs.download-context.outputs.pr_number }};
|
||||
const pullRequest = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number,
|
||||
});
|
||||
if (!pullRequest.data.node_id) throw new Error(`pullRequestId no found for '${pull_number}'`);
|
||||
return pullRequest.data.node_id;
|
||||
}
|
||||
const query = `
|
||||
mutation($id: ID!) {
|
||||
convertPullRequestToDraft(input: { pullRequestId: $id }) {
|
||||
pullRequest {
|
||||
id
|
||||
number
|
||||
isDraft
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const pullRequestId = await getPullRequestId();
|
||||
const variables = {
|
||||
id: pullRequestId,
|
||||
}
|
||||
const response = await github.graphql(query, variables)
|
||||
if (!response.convertPullRequestToDraft) {
|
||||
throw new Error("Failed to convert pull request to draft");
|
||||
}
|
||||
console.info("Pull request successfully converted to draft.");
|
||||
console.info(`Draft conversion response: ${JSON.stringify(response, null, 2)}`);
|
||||
@@ -1,17 +0,0 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
# Pattern matched against refs/tags
|
||||
tags:
|
||||
- "*" # Push events to every tag not containing /
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: main # Always checkout main even for tagged releases
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
- run: git push origin +main:production
|
||||
+550
File diff suppressed because one or more lines are too long
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -5,5 +5,7 @@ plugins:
|
||||
spec: "@yarnpkg/plugin-interactive-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
|
||||
spec: "@yarnpkg/plugin-workspace-tools"
|
||||
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
|
||||
spec: "@yarnpkg/plugin-version"
|
||||
|
||||
yarnPath: .yarn/releases/yarn-3.4.1.cjs
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts"],
|
||||
"setupFiles": ["<rootDir>/test/setEnvVars.ts", "jest-date-mock"],
|
||||
"reporters": ["default", "jest-summarizing-reporter"],
|
||||
"workerIdleMemoryLimit": "512MB",
|
||||
"maxWorkers": 2
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"start": "nest start",
|
||||
"dev:build:watch": "yarn workspace @calcom/platform-constants build:watch & yarn workspace @calcom/platform-utils build:watch & yarn workspace @calcom/platform-types build:watch",
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-enums build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build ",
|
||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
||||
"dev": "yarn dev:build && ts-node scripts/docker-start.ts && yarn copy-swagger-module && yarn start --watch",
|
||||
"dev:no-docker": "yarn dev:build && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.61",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.63",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -87,6 +87,7 @@
|
||||
"@types/passport-jwt": "^3.0.13",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"jest": "^29.7.0",
|
||||
"jest-date-mock": "^1.0.10",
|
||||
"prettier": "^2.8.6",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^6.3.3",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
function checkCommandExists(command: string): boolean {
|
||||
try {
|
||||
execSync(`${command} --version`, { stdio: "ignore" });
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if docker is installed
|
||||
if (!checkCommandExists("docker")) {
|
||||
throw new Error("Docker is not installed");
|
||||
}
|
||||
|
||||
// Try docker compose first (new syntax)
|
||||
try {
|
||||
execSync("docker compose version", { stdio: "ignore" });
|
||||
console.log("Starting containers with docker compose...");
|
||||
execSync("docker compose up -d", { stdio: "inherit" });
|
||||
} catch (e) {
|
||||
// Fall back to docker-compose if the above fails
|
||||
if (checkCommandExists("docker-compose")) {
|
||||
console.log("Starting containers with docker-compose...");
|
||||
execSync("docker-compose up -d", { stdio: "inherit" });
|
||||
} else {
|
||||
throw new Error("Neither 'docker compose' nor 'docker-compose' command is available");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error occurred"}`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -160,10 +160,10 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/")
|
||||
async createBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() body: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Body() body: CreateBookingInput_2024_04_15
|
||||
): Promise<ApiResponse<Partial<BookingResponse>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
const { orgSlug, locationUrl } = body;
|
||||
req.headers["x-cal-force-slug"] = orgSlug;
|
||||
try {
|
||||
@@ -191,10 +191,10 @@ export class BookingsController_2024_04_15 {
|
||||
async cancelBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() _: CancelBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Body() _: CancelBookingInput_2024_04_15
|
||||
): Promise<ApiResponse<{ bookingId: number; bookingUid: string; onlyRemovedAttendee: boolean }>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
if (bookingId) {
|
||||
try {
|
||||
req.body.id = parseInt(bookingId);
|
||||
@@ -245,10 +245,10 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/recurring")
|
||||
async createRecurringBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[],
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Body() _: CreateRecurringBookingInput_2024_04_15[]
|
||||
): Promise<ApiResponse<BookingResponse[]>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
try {
|
||||
const recurringEventId = uuidv4();
|
||||
for (const recurringEvent of req.body) {
|
||||
@@ -283,10 +283,10 @@ export class BookingsController_2024_04_15 {
|
||||
@Post("/instant")
|
||||
async createInstantBooking(
|
||||
@Req() req: BookingRequest,
|
||||
@Body() _: CreateBookingInput_2024_04_15,
|
||||
@Headers(X_CAL_CLIENT_ID) clientId?: string
|
||||
@Body() _: CreateBookingInput_2024_04_15
|
||||
): Promise<ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>>> {
|
||||
const oAuthClientId = clientId?.toString();
|
||||
const clientIdHeader = req.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientId = clientIdHeader?.toString();
|
||||
req.userId = (await this.getOwnerId(req)) ?? -1;
|
||||
try {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
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 { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("With api key", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let apiKeyString: string;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||
apiKeyString = keyString;
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||
user.id
|
||||
);
|
||||
eventTypeId = event.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
describe("create bookings", () => {
|
||||
it("should create a booking with api key", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Key",
|
||||
email: "mr_key@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(eventTypeId);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,291 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerScheduledEmail,
|
||||
AttendeeRescheduledEmail,
|
||||
OrganizerRescheduledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
OrganizerCancelledEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
CancelBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("With api key", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let apiKeyString: string;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
|
||||
let createdBooking: BookingOutput_2024_08_13;
|
||||
let rescheduledBooking: BookingOutput_2024_08_13;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
|
||||
apiKeyString = keyString;
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||
user.id
|
||||
);
|
||||
eventTypeId = event.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a booking with api key", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Key",
|
||||
email: "mr_key@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString());
|
||||
expect(data.duration).toEqual(60);
|
||||
expect(data.eventTypeId).toEqual(eventTypeId);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
createdBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should reschedule a booking with api key", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${apiKeyString}` })
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(createdBooking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual(createdBooking.status);
|
||||
expect(data.duration).toEqual(createdBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
expect(data.metadata).toEqual(createdBooking.metadata);
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
rescheduledBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
const booking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
|
||||
expect(booking).toBeDefined();
|
||||
expect(booking?.status).toEqual("ACCEPTED");
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${rescheduledBooking.uid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.status).toEqual("cancelled");
|
||||
expect(data.cancellationReason).toEqual(body.cancellationReason);
|
||||
expect(data.start).toEqual(rescheduledBooking.start);
|
||||
expect(data.end).toEqual(rescheduledBooking.end);
|
||||
expect(data.duration).toEqual(rescheduledBooking.duration);
|
||||
expect(data.eventTypeId).toEqual(rescheduledBooking.eventTypeId);
|
||||
expect(data.attendees[0]).toEqual(rescheduledBooking.attendees[0]);
|
||||
expect(data.location).toEqual(rescheduledBooking.location);
|
||||
expect(data.absentHost).toEqual(rescheduledBooking.absentHost);
|
||||
|
||||
const cancelledBooking = await bookingsRepositoryFixture.getByUid(rescheduledBooking.uid);
|
||||
expect(cancelledBooking).toBeDefined();
|
||||
expect(cancelledBooking?.status).toEqual("CANCELLED");
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
+5
-18
@@ -1,6 +1,5 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -20,26 +19,9 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerScheduledEmail,
|
||||
AttendeeDeclinedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { BookingOutput_2024_08_13, GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
jest.spyOn(AttendeeDeclinedEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Bookings confirmation", () => {
|
||||
let app: INestApplication;
|
||||
@@ -87,6 +69,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
+474
@@ -0,0 +1,474 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
OrganizerScheduledEmail,
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerRescheduledEmail,
|
||||
AttendeeRescheduledEmail,
|
||||
OrganizerCancelledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
AttendeeRequestEmail,
|
||||
OrganizerRequestEmail,
|
||||
AttendeeDeclinedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
jest
|
||||
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeRequestEmail.prototype, "getHtmlRequestEmail")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerRequestEmail.prototype, "getHtmlRequestEmail")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeDeclinedEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type EmailSetup = {
|
||||
user: User;
|
||||
eventTypeId: number;
|
||||
createdBookingUid: string;
|
||||
rescheduledBookingUid: string;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 confirm emails", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
let emailsEnabledSetup: EmailSetup;
|
||||
let emailsDisabledSetup: EmailSetup;
|
||||
|
||||
const authEmail = `admin-${Math.floor(Math.random() * 1000)}@example.com`;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
authEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
|
||||
await setupEnabledEmails();
|
||||
await setupDisabledEmails();
|
||||
|
||||
await userRepositoryFixture.create({
|
||||
email: authEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupEnabledEmails() {
|
||||
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
|
||||
|
||||
const user = await userRepositoryFixture.create({
|
||||
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsEnabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
|
||||
user.id
|
||||
);
|
||||
|
||||
emailsEnabledSetup = {
|
||||
user,
|
||||
eventTypeId: event.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function setupDisabledEmails() {
|
||||
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
const user = await userRepositoryFixture.create({
|
||||
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsDisabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60, requiresConfirmation: true },
|
||||
user.id
|
||||
);
|
||||
|
||||
emailsDisabledSetup = {
|
||||
user,
|
||||
eventTypeId: event.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: emailsEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
describe("OAuth client managed user bookings - emails disabled", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should not send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsDisabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when confirming a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsDisabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when declining a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeDeclinedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth client managed user bookings - emails enabled", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when confirming a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when creating a booking that requires confirmation", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(responseBody.data.status).toEqual("pending");
|
||||
expect(AttendeeRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
expect(OrganizerRequestEmail.prototype.getHtmlRequestEmail).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when declining a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeDeclinedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(authEmail);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsEnabledSetup.user.id,
|
||||
emailsEnabledSetup.user.email
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsDisabledSetup.user.id,
|
||||
emailsDisabledSetup.user.email
|
||||
);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
+979
@@ -0,0 +1,979 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { createParamDecorator, 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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
OrganizerScheduledEmail,
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerRescheduledEmail,
|
||||
AttendeeRescheduledEmail,
|
||||
OrganizerCancelledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
OrganizerReassignedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
jest
|
||||
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
jest
|
||||
.spyOn(OrganizerReassignedEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type EmailSetup = {
|
||||
team: Team;
|
||||
member1: User;
|
||||
member2: User;
|
||||
collectiveEventType: {
|
||||
id: number;
|
||||
createdBookingUid: string;
|
||||
rescheduledBookingUid: string;
|
||||
};
|
||||
roundRobinEventType: {
|
||||
id: number;
|
||||
createdBookingUid: string;
|
||||
rescheduledBookingUid: string;
|
||||
currentHostId: number;
|
||||
};
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 team emails", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
|
||||
let emailsEnabledSetup: EmailSetup;
|
||||
let emailsDisabledSetup: EmailSetup;
|
||||
|
||||
const authEmail = "admin@example.com";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
authEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
|
||||
await setupEnabledEmails();
|
||||
await setupDisabledEmails();
|
||||
|
||||
await userRepositoryFixture.create({
|
||||
email: authEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupEnabledEmails() {
|
||||
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
|
||||
|
||||
const team = await teamRepositoryFixture.create({
|
||||
name: "team 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsEnabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const member1 = await userRepositoryFixture.create({
|
||||
email: "alice@gmail.com",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsEnabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const member2 = await userRepositoryFixture.create({
|
||||
email: "bob@gmail.com",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsEnabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(member1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(member2.id, userSchedule);
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${member1.id}`,
|
||||
username: member1.email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: member1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${member2.id}`,
|
||||
username: member2.email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: member2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: member1.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: member2.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const collectiveEvent = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
title: "Collective Event Type",
|
||||
slug: "collective-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: member1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: collectiveEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: member2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: collectiveEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roundRobinEvent = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
title: "Round Robin Event Type",
|
||||
slug: "round-robin-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: member1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: roundRobinEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: member2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: roundRobinEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
emailsEnabledSetup = {
|
||||
team,
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
collectiveEventType: {
|
||||
id: collectiveEvent.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
},
|
||||
roundRobinEventType: {
|
||||
id: roundRobinEvent.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
currentHostId: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function setupDisabledEmails() {
|
||||
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
const team = await teamRepositoryFixture.create({
|
||||
name: "team 2",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsDisabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const member1 = await userRepositoryFixture.create({
|
||||
email: "charlie@gmail.com",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsDisabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const member2 = await userRepositoryFixture.create({
|
||||
email: "dean@gmail.com",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsDisabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(member1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(member2.id, userSchedule);
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${member1.id}`,
|
||||
username: member1.email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: member1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${member2.id}`,
|
||||
username: member2.email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: member2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: member1.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: member2.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const collectiveEvent = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
title: "Collective Event Type",
|
||||
slug: "collective-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: member1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: collectiveEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: true,
|
||||
user: {
|
||||
connect: {
|
||||
id: member2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: collectiveEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const roundRobinEvent = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
title: "Round Robin Event Type",
|
||||
slug: "round-robin-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: member1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: roundRobinEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: member2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: roundRobinEvent.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
emailsDisabledSetup = {
|
||||
team,
|
||||
member1: member1,
|
||||
member2: member2,
|
||||
collectiveEventType: {
|
||||
id: collectiveEvent.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
},
|
||||
roundRobinEventType: {
|
||||
id: roundRobinEvent.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
currentHostId: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: emailsEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("OAuth client team bookings - emails disabled", () => {
|
||||
describe("book", () => {
|
||||
it("should not send an email when booking collective event", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsDisabledSetup.collectiveEventType.id,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when booking round robin event", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsDisabledSetup.roundRobinEventType.id,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.createdBookingUid = responseBody.data.uid;
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule", () => {
|
||||
it("should not send an email when rescheduling collective booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.collectiveEventType.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.collectiveEventType.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when rescheduling round robin booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reassign", () => {
|
||||
it("should not send an email when manually reassigning round robin booking", async () => {
|
||||
const reassignToId =
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId === emailsDisabledSetup.member1.id
|
||||
? emailsDisabledSetup.member2.id
|
||||
: emailsDisabledSetup.member1.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(
|
||||
`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign/${reassignToId}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when automatically reassigning round robin booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerReassignedEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel", () => {
|
||||
it("should not send an email when cancelling a collective booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.collectiveEventType.rescheduledBookingUid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when cancelling a round robin booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.roundRobinEventType.rescheduledBookingUid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth client team bookings - emails enabled", () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("book", () => {
|
||||
it("should send an email when booking collective event", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.collectiveEventType.id,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsEnabledSetup.collectiveEventType.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when booking round robin event", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.roundRobinEventType.id,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsEnabledSetup.roundRobinEventType.createdBookingUid = responseBody.data.uid;
|
||||
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule", () => {
|
||||
it("should send an email when rescheduling collective booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 11, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.collectiveEventType.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsEnabledSetup.collectiveEventType.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when rescheduling round robin booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 12, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid = responseBody.data.uid;
|
||||
emailsEnabledSetup.roundRobinEventType.currentHostId = responseBody.data.hosts[0].id;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("reassign", () => {
|
||||
it("should send an email when manually reassigning round robin booking", async () => {
|
||||
const reassignToId =
|
||||
emailsEnabledSetup.roundRobinEventType.currentHostId === emailsEnabledSetup.member1.id
|
||||
? emailsEnabledSetup.member2.id
|
||||
: emailsEnabledSetup.member1.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(
|
||||
`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign/${reassignToId}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsDisabledSetup.roundRobinEventType.currentHostId = reassignToId;
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when automatically reassigning round robin booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/reassign`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerReassignedEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel", () => {
|
||||
it("should send an email when cancelling a collective booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.collectiveEventType.rescheduledBookingUid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when cancelling round robin booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.roundRobinEventType.rescheduledBookingUid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.member1.email);
|
||||
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.member1.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsEnabledSetup.member1.id,
|
||||
emailsEnabledSetup.member1.email
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsDisabledSetup.member1.id,
|
||||
emailsDisabledSetup.member1.email
|
||||
);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
+482
@@ -0,0 +1,482 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.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 { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.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 { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
OrganizerScheduledEmail,
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerRescheduledEmail,
|
||||
AttendeeRescheduledEmail,
|
||||
OrganizerCancelledEmail,
|
||||
AttendeeCancelledEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { CancelBookingInput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
jest
|
||||
.spyOn(AttendeeScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerScheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerRescheduledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(AttendeeCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
jest
|
||||
.spyOn(OrganizerCancelledEmail.prototype, "getHtml")
|
||||
.mockImplementation(() => Promise.resolve("<p>email</p>"));
|
||||
|
||||
type EmailSetup = {
|
||||
user: User;
|
||||
eventTypeId: number;
|
||||
recurringEventTypeId: number;
|
||||
createdBookingUid: string;
|
||||
rescheduledBookingUid: string;
|
||||
};
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13 user emails", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
let emailsEnabledSetup: EmailSetup;
|
||||
let emailsDisabledSetup: EmailSetup;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.overrideGuard(ApiAuthGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
|
||||
await setupEnabledEmails();
|
||||
await setupDisabledEmails();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function setupEnabledEmails() {
|
||||
const oAuthClientEmailsEnabled = await createOAuthClient(organization.id, true);
|
||||
|
||||
const user = await userRepositoryFixture.create({
|
||||
email: `alice-${Math.floor(Math.random() * 1000)}@gmail.com`,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsEnabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||
user.id
|
||||
);
|
||||
|
||||
const recurringEvent = await eventTypesRepositoryFixture.create(
|
||||
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
|
||||
{
|
||||
title: "peer coding recurring",
|
||||
slug: "peer-coding-recurring",
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: 3, interval: 1 },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
emailsEnabledSetup = {
|
||||
user,
|
||||
eventTypeId: event.id,
|
||||
recurringEventTypeId: recurringEvent.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function setupDisabledEmails() {
|
||||
const oAuthClientEmailsDisabled = await createOAuthClient(organization.id, false);
|
||||
|
||||
const user = await userRepositoryFixture.create({
|
||||
email: `bob-${Math.floor(Math.random() * 1000)}@gmail.com`,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClientEmailsDisabled.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: "peer-coding", length: 60 },
|
||||
user.id
|
||||
);
|
||||
|
||||
const recurringEvent = await eventTypesRepositoryFixture.create(
|
||||
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
|
||||
{
|
||||
title: "peer coding recurring",
|
||||
slug: "peer-coding-recurring",
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: 3, interval: 1 },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
emailsDisabledSetup = {
|
||||
user,
|
||||
eventTypeId: event.id,
|
||||
recurringEventTypeId: recurringEvent.id,
|
||||
createdBookingUid: "",
|
||||
rescheduledBookingUid: "",
|
||||
};
|
||||
}
|
||||
|
||||
async function createOAuthClient(organizationId: number, emailsEnabled: boolean) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
areEmailsEnabled: emailsEnabled,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("OAuth client managed user bookings - emails disabled", () => {
|
||||
it("should not send an email when creating a booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsDisabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when creating a recurring booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsDisabledSetup.recurringEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking array but received single booking");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when rescheduling a booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
emailsDisabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
it("should not send an email when cancelling a booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsDisabledSetup.rescheduledBookingUid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("OAuth client managed user bookings - emails enabled", () => {
|
||||
it("should send an email when creating a booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
emailsEnabledSetup.createdBookingUid = responseBody.data.uid;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when creating a recurring booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 9, 0, 0)).toISOString(),
|
||||
eventTypeId: emailsEnabledSetup.recurringEventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
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: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
if (responseDataIsRecurringBooking(responseBody.data)) {
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
} else {
|
||||
throw new Error("Invalid response data - expected booking array but received single booking");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when rescheduling a booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.createdBookingUid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
expect(OrganizerScheduledEmail.prototype.getHtml).not.toHaveBeenCalled();
|
||||
|
||||
expect(AttendeeRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerRescheduledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
|
||||
emailsEnabledSetup.rescheduledBookingUid = responseBody.data.uid;
|
||||
});
|
||||
});
|
||||
|
||||
it("should send an email when cancelling a booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
cancellationReason: "Going on a vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${emailsEnabledSetup.rescheduledBookingUid}/cancel`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: CancelBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(AttendeeCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
expect(OrganizerCancelledEmail.prototype.getHtml).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(emailsEnabledSetup.user.email);
|
||||
await userRepositoryFixture.deleteByEmail(emailsDisabledSetup.user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsEnabledSetup.user.id,
|
||||
emailsEnabledSetup.user.email
|
||||
);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(
|
||||
emailsDisabledSetup.user.id,
|
||||
emailsDisabledSetup.user.email
|
||||
);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
});
|
||||
+7
-3
@@ -1,6 +1,5 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
@@ -80,14 +79,19 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: "team 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
teamUser1 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail,
|
||||
locale: "it",
|
||||
+15
@@ -72,6 +72,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
@@ -375,6 +380,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
@@ -577,6 +587,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
+37
@@ -93,6 +93,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
@@ -163,6 +168,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -205,6 +213,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
@@ -230,6 +239,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "Rust",
|
||||
},
|
||||
metadata: {
|
||||
userId: "200",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -274,6 +286,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: createdSeatedBooking.attendees[0].name,
|
||||
...createdSeatedBooking.attendees[0].bookingFieldsResponses,
|
||||
},
|
||||
metadata: createdSeatedBooking.attendees[0].metadata,
|
||||
});
|
||||
const secondAttendee = data.attendees.find((attendee) => attendee.name === nameAttendeeTwo);
|
||||
expect(secondAttendee).toEqual({
|
||||
@@ -286,6 +299,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
@@ -314,6 +328,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
recurrenceCount,
|
||||
metadata: {
|
||||
userId: "300",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -359,6 +376,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
@@ -392,6 +410,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
@@ -565,6 +584,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: attendee?.name,
|
||||
...attendee?.bookingFieldsResponses,
|
||||
},
|
||||
metadata: attendee?.metadata,
|
||||
});
|
||||
expect(data.location).toBeDefined();
|
||||
expect(data.absentHost).toEqual(false);
|
||||
@@ -697,6 +717,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
@@ -769,6 +794,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "1000",
|
||||
},
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
@@ -815,6 +843,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
@@ -848,6 +877,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
@@ -875,6 +905,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
metadata: {
|
||||
userId: "2000",
|
||||
},
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
@@ -921,6 +954,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
expect(firstBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
@@ -932,6 +966,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
@@ -965,6 +1000,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
expect(secondBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
@@ -976,6 +1012,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
+22
-2
@@ -85,30 +85,50 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
team1 = await teamRepositoryFixture.create({
|
||||
name: "team 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
team2 = await teamRepositoryFixture.create({
|
||||
name: "team 2",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
createdByOAuthClient: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
teamUser = await userRepositoryFixture.create({
|
||||
email: teamUserEmail,
|
||||
locale: "it",
|
||||
name: "orgUser1team1",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
teamUser2 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail2,
|
||||
locale: "es",
|
||||
name: "orgUser2team1",
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
+174
@@ -14,6 +14,7 @@ import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import { advanceTo, clear } from "jest-date-mock";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
@@ -95,6 +96,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
@@ -178,6 +184,136 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("create bookings", () => {
|
||||
describe("invalid metadata", () => {
|
||||
it("should not be able to create a booking with metadata with more than 50 keys", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
key1: "1",
|
||||
key2: "2",
|
||||
key3: "3",
|
||||
key4: "4",
|
||||
key5: "5",
|
||||
key6: "6",
|
||||
key7: "7",
|
||||
key8: "8",
|
||||
key9: "9",
|
||||
key10: "10",
|
||||
key11: "11",
|
||||
key12: "12",
|
||||
key13: "13",
|
||||
key14: "14",
|
||||
key15: "15",
|
||||
key16: "16",
|
||||
key17: "17",
|
||||
key18: "18",
|
||||
key19: "19",
|
||||
key20: "20",
|
||||
key21: "21",
|
||||
key22: "22",
|
||||
key23: "23",
|
||||
key24: "24",
|
||||
key25: "25",
|
||||
key26: "26",
|
||||
key27: "27",
|
||||
key28: "28",
|
||||
key29: "29",
|
||||
key30: "30",
|
||||
key31: "31",
|
||||
key32: "32",
|
||||
key33: "33",
|
||||
key34: "34",
|
||||
key35: "35",
|
||||
key36: "36",
|
||||
key37: "37",
|
||||
key38: "38",
|
||||
key39: "39",
|
||||
key40: "40",
|
||||
key41: "41",
|
||||
key42: "42",
|
||||
key43: "43",
|
||||
key44: "44",
|
||||
key45: "45",
|
||||
key46: "46",
|
||||
key47: "47",
|
||||
key48: "48",
|
||||
key49: "49",
|
||||
key50: "50",
|
||||
key51: "51",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should not be able to create a booking with metadata with a key longer than 40 characters", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
aaaaaaaaaabbbbbbbbbbccccccccccdddddddddde: "1",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should not be able to create a booking with metadata with a value longer than 500 characters", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
key: `${"a".repeat(501)}`,
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
@@ -192,14 +328,19 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
metadata: {
|
||||
userId: "100",
|
||||
},
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const afterCreate = new Date();
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
@@ -233,6 +374,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
email: body.attendee.email,
|
||||
...body.bookingFieldsResponses,
|
||||
});
|
||||
|
||||
// Check createdAt date is between the time of the request and after the request
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
|
||||
expect(data.metadata).toEqual(body.metadata);
|
||||
createdBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
@@ -253,6 +401,9 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
metadata: {
|
||||
userId: "1000",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
@@ -288,6 +439,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.recurringBookingUid).toBeDefined();
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
expect(firstBooking.metadata).toEqual(body.metadata);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
@@ -307,6 +459,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
expect(secondBooking.metadata).toEqual(body.metadata);
|
||||
|
||||
const thirdBooking = data[2];
|
||||
expect(thirdBooking.id).toBeDefined();
|
||||
@@ -326,6 +479,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(thirdBooking.location).toEqual(body.location);
|
||||
expect(thirdBooking.absentHost).toEqual(false);
|
||||
expect(thirdBooking.metadata).toEqual(body.metadata);
|
||||
|
||||
createdRecurringBooking = data;
|
||||
} else {
|
||||
@@ -362,6 +516,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
expect(data.createdAt).toEqual(createdBooking.createdAt);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
@@ -835,12 +990,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
const beforeCreate = new Date();
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdBooking.uid}/reschedule`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const afterCreate = new Date();
|
||||
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
@@ -860,6 +1017,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees[0]).toEqual(createdBooking.attendees[0]);
|
||||
expect(data.location).toEqual(createdBooking.location);
|
||||
expect(data.absentHost).toEqual(createdBooking.absentHost);
|
||||
expect(data.metadata).toEqual(createdBooking.metadata);
|
||||
|
||||
// When a booking is rescheduled, a new booking is created and the old booking is cancelled.
|
||||
// We want to make sure the createdAt date of the new booking is between the beforeCreate and afterCreate dates.
|
||||
const createdAtDate = new Date(data.createdAt);
|
||||
expect(createdAtDate.getTime()).toBeGreaterThanOrEqual(beforeCreate.getTime());
|
||||
expect(createdAtDate.getTime()).toBeLessThanOrEqual(afterCreate.getTime());
|
||||
|
||||
rescheduledBooking = data;
|
||||
});
|
||||
@@ -913,6 +1077,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
expect(data.attendees[0]).toEqual(createdRecurringBooking[0].attendees[0]);
|
||||
expect(data.location).toEqual(createdRecurringBooking[0].location);
|
||||
expect(data.absentHost).toEqual(createdRecurringBooking[0].absentHost);
|
||||
expect(data.metadata).toEqual(createdRecurringBooking[0].metadata);
|
||||
|
||||
const oldBooking = await bookingsRepositoryFixture.getByUid(createdRecurringBooking[0].uid);
|
||||
expect(oldBooking).toBeDefined();
|
||||
@@ -952,6 +1117,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("mark absent", () => {
|
||||
beforeAll(() => {
|
||||
advanceTo(new Date(2035, 0, 9, 15, 0, 0));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
clear();
|
||||
});
|
||||
|
||||
it("should mark host absent", async () => {
|
||||
const body: MarkAbsentBookingInput_2024_08_13 = {
|
||||
host: true,
|
||||
@@ -1018,6 +1191,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancel bookings", () => {
|
||||
it("should cancel booking", async () => {
|
||||
const body: CancelBookingInput_2024_08_13 = {
|
||||
+5
-3
@@ -1,9 +1,6 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
@@ -71,6 +68,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
platformOAuthClients: {
|
||||
connect: {
|
||||
id: oAuthClient.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
@@ -389,11 +389,14 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(requestUser);
|
||||
|
||||
await roundRobinReassignment({
|
||||
bookingId: booking.id,
|
||||
orgId: profile?.organizationId || null,
|
||||
emailsEnabled,
|
||||
});
|
||||
|
||||
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||
@@ -404,6 +407,12 @@ export class BookingsService_2024_08_13 {
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
|
||||
async getEmailsEnabled(eventTypeId: number) {
|
||||
const oAuthParams = await this.inputService.getOAuthClientParams(eventTypeId);
|
||||
const emailsEnabled = oAuthParams ? oAuthParams.arePlatformEmailsEnabled : true;
|
||||
return emailsEnabled;
|
||||
}
|
||||
|
||||
async reassignBookingToUser(
|
||||
bookingUid: string,
|
||||
newUserId: number,
|
||||
@@ -420,6 +429,8 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
const reassigned = await roundRobinManualReassignment({
|
||||
@@ -428,6 +439,7 @@ export class BookingsService_2024_08_13 {
|
||||
orgId: profile?.organizationId || null,
|
||||
reassignReason: body.reason,
|
||||
reassignedById,
|
||||
emailsEnabled,
|
||||
});
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
@@ -439,6 +451,8 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
user: requestUser,
|
||||
@@ -447,6 +461,7 @@ export class BookingsService_2024_08_13 {
|
||||
bookingId: booking.id,
|
||||
confirmed: true,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
emailsEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -459,6 +474,8 @@ export class BookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const emailsEnabled = booking.eventTypeId ? await this.getEmailsEnabled(booking.eventTypeId) : true;
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
user: requestUser,
|
||||
@@ -468,6 +485,7 @@ export class BookingsService_2024_08_13 {
|
||||
confirmed: false,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
reason,
|
||||
emailsEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
@@ -33,19 +32,10 @@ import {
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
import { EventType, PlatformOAuthClient } from "@calcom/prisma/client";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
|
||||
const DEFAULT_PLATFORM_PARAMS = {
|
||||
platformClientId: "",
|
||||
platformCancelUrl: "",
|
||||
platformRescheduleUrl: "",
|
||||
platformBookingUrl: "",
|
||||
arePlatformEmailsEnabled: false,
|
||||
platformBookingLocation: undefined,
|
||||
};
|
||||
|
||||
type OAuthRequestParams = {
|
||||
platformClientId: string;
|
||||
platformRescheduleUrl: string;
|
||||
@@ -92,30 +82,53 @@ export class InputBookingsService_2024_08_13 {
|
||||
body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13
|
||||
): Promise<BookingRequest> {
|
||||
const bodyTransformed = await this.transformInputCreateBooking(body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
const requestId = request.get("X-Request-Id");
|
||||
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
this.logger.log(`createBookingRequest_2024_04_15`, {
|
||||
requestId,
|
||||
requestId: request.get("X-Request-Id"),
|
||||
ownerId: userId,
|
||||
location,
|
||||
oAuthClientId,
|
||||
...oAuthParams,
|
||||
oAuthClientParams,
|
||||
});
|
||||
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
|
||||
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false };
|
||||
}
|
||||
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
async getOAuthClientParams(eventTypeId: number) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeById(eventTypeId);
|
||||
|
||||
let oAuthClient: PlatformOAuthClient | null = null;
|
||||
if (eventType?.userId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByUserId(eventType.userId);
|
||||
} else if (eventType?.teamId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
|
||||
}
|
||||
|
||||
if (oAuthClient) {
|
||||
return {
|
||||
platformClientId: oAuthClient.id,
|
||||
platformCancelUrl: oAuthClient.bookingCancelRedirectUri,
|
||||
platformRescheduleUrl: oAuthClient.bookingRescheduleRedirectUri,
|
||||
platformBookingUrl: oAuthClient.bookingRedirectUri,
|
||||
arePlatformEmailsEnabled: oAuthClient.areEmailsEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async transformInputCreateBooking(inputBooking: CreateBookingInput_2024_08_13) {
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(
|
||||
inputBooking.eventTypeId
|
||||
@@ -139,9 +152,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
eventTypeId: inputBooking.eventTypeId,
|
||||
timeZone: inputBooking.attendee.timeZone,
|
||||
language: inputBooking.attendee.language || "en",
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// metadata: inputBooking.metadata || {},
|
||||
metadata: {},
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: inputBooking.guests,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
@@ -180,21 +191,23 @@ export class InputBookingsService_2024_08_13 {
|
||||
): Promise<BookingRequest> {
|
||||
// note(Lauris): update to this.transformInputCreate when rescheduling is implemented
|
||||
const bodyTransformed = await this.transformInputCreateRecurringBooking(body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
const oAuthClientParams = await this.getOAuthClientParams(body.eventTypeId);
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
Object.assign(newRequest, {
|
||||
userId,
|
||||
...oAuthParams,
|
||||
platformBookingLocation: location,
|
||||
noEmail: !oAuthParams.arePlatformEmailsEnabled,
|
||||
});
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, {
|
||||
userId,
|
||||
...oAuthClientParams,
|
||||
platformBookingLocation: location,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
});
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
}
|
||||
|
||||
newRequest.body = bodyTransformed.map((event) => ({
|
||||
...event,
|
||||
@@ -243,9 +256,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
recurringEventId,
|
||||
timeZone: inputBooking.attendee.timeZone,
|
||||
language: inputBooking.attendee.language || "en",
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// metadata: inputBooking.metadata || {},
|
||||
metadata: {},
|
||||
metadata: inputBooking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: inputBooking.guests,
|
||||
// note(Lauris): responses with name and email are required by the handleNewBooking
|
||||
@@ -285,18 +296,20 @@ export class InputBookingsService_2024_08_13 {
|
||||
const bodyTransformed = this.isRescheduleSeatedBody(body)
|
||||
? await this.transformInputRescheduleSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputRescheduleBooking(bookingUid, body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
|
||||
const oAuthClientParams = await this.getOAuthClientParams(bodyTransformed.eventTypeId);
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
|
||||
const location = await this.getRescheduleBookingLocation(bookingUid);
|
||||
Object.assign(newRequest, { userId, ...oAuthParams, platformBookingLocation: location });
|
||||
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false };
|
||||
}
|
||||
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
@@ -345,9 +358,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
eventTypeId: eventType.id,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// metadata: booking.metadata || {},
|
||||
metadata: {},
|
||||
metadata: seat.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: [],
|
||||
responses: { ...bookingResponses },
|
||||
@@ -386,9 +397,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
eventTypeId: eventType.id,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// metadata: booking.metadata || {},
|
||||
metadata: {},
|
||||
metadata: booking.metadata || {},
|
||||
hasHashedBookingLink: false,
|
||||
guests: bookingResponses.guests,
|
||||
responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason },
|
||||
@@ -423,24 +432,6 @@ export class InputBookingsService_2024_08_13 {
|
||||
}
|
||||
}
|
||||
|
||||
private async createBookingRequestOAuthClientParams(clientId: string) {
|
||||
const params = DEFAULT_PLATFORM_PARAMS;
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
if (client) {
|
||||
params.platformClientId = clientId;
|
||||
params.platformCancelUrl = client.bookingCancelRedirectUri ?? "";
|
||||
params.platformRescheduleUrl = client.bookingRescheduleRedirectUri ?? "";
|
||||
params.platformBookingUrl = client.bookingRedirectUri ?? "";
|
||||
params.arePlatformEmailsEnabled = client.areEmailsEnabled ?? false;
|
||||
}
|
||||
return params;
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
transformGetBookingsFilters(queryParams: GetBookingsInput_2024_08_13) {
|
||||
return {
|
||||
attendeeEmail: queryParams.attendeeEmail,
|
||||
@@ -473,17 +464,26 @@ export class InputBookingsService_2024_08_13 {
|
||||
const bodyTransformed = this.isCancelSeatedBody(body)
|
||||
? await this.transformInputCancelSeatedBooking(bookingUid, body)
|
||||
: await this.transformInputCancelBooking(bookingUid, body);
|
||||
const oAuthClientId = request.get(X_CAL_CLIENT_ID);
|
||||
|
||||
const booking = await this.bookingsRepository.getByUid(bodyTransformed.uid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} not found`);
|
||||
}
|
||||
|
||||
const oAuthClientParams = booking.eventTypeId
|
||||
? await this.getOAuthClientParams(booking.eventTypeId)
|
||||
: undefined;
|
||||
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.createBookingRequestOAuthClientParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
|
||||
Object.assign(newRequest, { userId, ...oAuthParams });
|
||||
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled };
|
||||
} else {
|
||||
Object.assign(newRequest, { userId });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false };
|
||||
}
|
||||
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { plainToClass } from "class-transformer";
|
||||
import { DateTime } from "luxon";
|
||||
import { z } from "zod";
|
||||
|
||||
import { bookingMetadataSchema } from "@calcom/platform-libraries";
|
||||
import {
|
||||
BookingOutput_2024_08_13,
|
||||
CreateRecurringSeatedBookingOutput_2024_08_13,
|
||||
@@ -49,10 +50,21 @@ type DatabaseBooking = Booking & {
|
||||
noShow: boolean | null;
|
||||
bookingSeat?: BookingSeat | null;
|
||||
}[];
|
||||
} & { user: { id: number; name: string | null; email: string } | null };
|
||||
user: { id: number; name: string | null; email: string } | null;
|
||||
createdAt: Date;
|
||||
};
|
||||
|
||||
type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
const bookingUserMetadataSchema = bookingMetadataSchema.transform((data) => {
|
||||
if (data === null) return {};
|
||||
// note(Lauris): return only user defined metadata
|
||||
const { videoCallUrl, ...rest } = data;
|
||||
return rest;
|
||||
});
|
||||
|
||||
const seatedBookingMetadataSchema = z.object({}).catchall(z.string());
|
||||
|
||||
@Injectable()
|
||||
export class OutputBookingsService_2024_08_13 {
|
||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||
@@ -63,6 +75,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingUserMetadataSchema.parse(databaseBooking.metadata);
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -92,11 +105,13 @@ export class OutputBookingsService_2024_08_13 {
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
};
|
||||
|
||||
const bookingTransformed = plainToClass(BookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses so attaching manually
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
bookingTransformed.metadata = metadata;
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
@@ -121,6 +136,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
const duration = dateEnd.diff(dateStart, "minutes").minutes;
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(databaseBooking.responses);
|
||||
const metadata = bookingUserMetadataSchema.parse(databaseBooking.metadata);
|
||||
|
||||
const booking = {
|
||||
id: databaseBooking.id,
|
||||
@@ -152,9 +168,16 @@ export class OutputBookingsService_2024_08_13 {
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
bookingFieldsResponses: databaseBooking.responses,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
};
|
||||
|
||||
return plainToClass(RecurringBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
const bookingTransformed = plainToClass(RecurringBookingOutput_2024_08_13, booking, {
|
||||
strategy: "excludeAll",
|
||||
});
|
||||
// note(Lauris): I don't know why plainToClass erases bookings responses and metadata so attaching manually
|
||||
bookingTransformed.bookingFieldsResponses = bookingResponses;
|
||||
bookingTransformed.metadata = metadata;
|
||||
return bookingTransformed;
|
||||
}
|
||||
|
||||
getOutputCreateSeatedBooking(
|
||||
@@ -189,6 +212,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
// note(Lauris): meetingUrl is deprecated
|
||||
meetingUrl: databaseBooking.location,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetSeatedBookingOutput_2024_08_13, booking, { strategy: "excludeAll" });
|
||||
@@ -208,6 +232,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
@@ -282,6 +307,7 @@ export class OutputBookingsService_2024_08_13 {
|
||||
meetingUrl: databaseBooking.location,
|
||||
recurringBookingUid: databaseBooking.recurringEventId,
|
||||
absentHost: !!databaseBooking.noShowHost,
|
||||
createdAt: databaseBooking.createdAt,
|
||||
};
|
||||
|
||||
const parsed = plainToClass(GetRecurringSeatedBookingOutput_2024_08_13, booking, {
|
||||
@@ -303,9 +329,9 @@ export class OutputBookingsService_2024_08_13 {
|
||||
};
|
||||
const attendeeParsed = plainToClass(SeatedAttendee, attendeeData, { strategy: "excludeAll" });
|
||||
attendeeParsed.bookingFieldsResponses = responses || {};
|
||||
attendeeParsed.metadata = seatedBookingMetadataSchema.parse(attendee.bookingSeat?.metadata);
|
||||
// note(Lauris): as of now email is not returned for privacy
|
||||
delete attendeeParsed.bookingFieldsResponses.email;
|
||||
|
||||
return attendeeParsed;
|
||||
});
|
||||
|
||||
|
||||
@@ -99,4 +99,28 @@ export class OAuthClientRepository {
|
||||
where: { id: clientId },
|
||||
});
|
||||
}
|
||||
|
||||
async getByUserId(userId: number) {
|
||||
return this.dbRead.prisma.platformOAuthClient.findFirst({
|
||||
where: {
|
||||
users: {
|
||||
some: {
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByTeamId(teamId: number) {
|
||||
return this.dbRead.prisma.platformOAuthClient.findFirst({
|
||||
where: {
|
||||
teams: {
|
||||
some: {
|
||||
id: teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12850,6 +12850,13 @@
|
||||
"description": "Location for this booking. Displayed in email and calendar event.",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "You can store any additional data you want here. Metadata must have at most 50 keys, each key up to 40 characters, and string values up to 500 characters.",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -12913,6 +12920,13 @@
|
||||
"description": "Location for this booking. Displayed in email and calendar event.",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "You can store any additional data you want here. Metadata must have at most 50 keys, each key up to 40 characters, and string values up to 500 characters.",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -12982,6 +12996,13 @@
|
||||
"description": "Location for this booking. Displayed in email and calendar event.",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "You can store any additional data you want here. Metadata must have at most 50 keys, each key up to 40 characters, and string values up to 500 characters.",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -13101,6 +13122,16 @@
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -13138,6 +13169,7 @@
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"absentHost",
|
||||
"createdAt",
|
||||
"attendees"
|
||||
]
|
||||
},
|
||||
@@ -13223,6 +13255,16 @@
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string",
|
||||
"example": "2024-08-13T15:30:00Z"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -13264,6 +13306,7 @@
|
||||
"eventTypeId",
|
||||
"eventType",
|
||||
"absentHost",
|
||||
"createdAt",
|
||||
"attendees",
|
||||
"recurringBookingUid"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder";
|
||||
import type { ImmutableTree, BuilderProps } from "react-awesome-query-builder";
|
||||
import type { JsonTree } from "react-awesome-query-builder";
|
||||
|
||||
import {
|
||||
withRaqbSettingsAndWidgets,
|
||||
ConfigFor,
|
||||
} from "@calcom/app-store/routing-forms/components/react-awesome-query-builder/config/uiConfig";
|
||||
import { getQueryBuilderConfigForAttributes } from "@calcom/app-store/routing-forms/lib/getQueryBuilderConfig";
|
||||
import { classNames as cn } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { isEqual } from "@calcom/lib/isEqual";
|
||||
import { buildStateFromQueryValue } from "@calcom/lib/raqb/raqbUtils";
|
||||
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
|
||||
export type Attributes = RouterOutputs["viewer"]["appRoutingForms"]["getAttributesForTeam"];
|
||||
export function useAttributes(teamId: number) {
|
||||
const { data: attributes, isPending } = trpc.viewer.appRoutingForms.getAttributesForTeam.useQuery({
|
||||
teamId,
|
||||
});
|
||||
return {
|
||||
attributes,
|
||||
isPending,
|
||||
};
|
||||
}
|
||||
|
||||
function SegmentWithAttributes({
|
||||
attributes,
|
||||
teamId,
|
||||
queryValue: initialQueryValue,
|
||||
onQueryValueChange,
|
||||
className,
|
||||
}: {
|
||||
attributes: Attributes;
|
||||
teamId: number;
|
||||
queryValue: AttributesQueryValue | null;
|
||||
onQueryValueChange: ({ queryValue }: { queryValue: AttributesQueryValue }) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const attributesQueryBuilderConfig = getQueryBuilderConfigForAttributes({
|
||||
attributes,
|
||||
});
|
||||
|
||||
const [queryValue, setQueryValue] = useState(initialQueryValue);
|
||||
const attributesQueryBuilderConfigWithRaqbSettingsAndWidgets = withRaqbSettingsAndWidgets({
|
||||
config: attributesQueryBuilderConfig,
|
||||
configFor: ConfigFor.Attributes,
|
||||
});
|
||||
|
||||
const queryBuilderData = buildStateFromQueryValue({
|
||||
queryValue: queryValue as JsonTree,
|
||||
config: attributesQueryBuilderConfigWithRaqbSettingsAndWidgets,
|
||||
});
|
||||
|
||||
const renderBuilder = useCallback(
|
||||
(props: BuilderProps) => (
|
||||
<div className="query-builder-container" data-testid="query-builder-container">
|
||||
<div className="query-builder qb-lite">
|
||||
<Builder {...props} />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
function onChange(immutableTree: ImmutableTree) {
|
||||
const jsonTree = QbUtils.getTree(immutableTree) as AttributesQueryValue;
|
||||
|
||||
// IMPORTANT: RAQB calls onChange even without explicit user action. It just identifies if the props have changed or not. isEqual ensures that we don't end up having infinite re-renders.
|
||||
if (!isEqual(jsonTree, queryValue)) {
|
||||
setQueryValue(jsonTree);
|
||||
onQueryValueChange({
|
||||
queryValue: jsonTree,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// cal-query-builder class has special styling through global CSS, allowing us to customize RAQB
|
||||
<div>
|
||||
<div className={cn("cal-query-builder", className)}>
|
||||
<Query
|
||||
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
|
||||
value={queryBuilderData.state.tree}
|
||||
onChange={onChange}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-sm">
|
||||
<MatchingTeamMembers teamId={teamId} queryValue={queryValue} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MatchingTeamMembers({
|
||||
teamId,
|
||||
queryValue,
|
||||
}: {
|
||||
teamId: number;
|
||||
queryValue: AttributesQueryValue | null;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const { data: matchingTeamMembersWithResult, isPending } =
|
||||
trpc.viewer.attributes.findTeamMembersMatchingAttributeLogic.useQuery({
|
||||
teamId,
|
||||
attributesQueryValue: queryValue,
|
||||
_enablePerf: true,
|
||||
});
|
||||
|
||||
if (isPending) return <span>{t("loading")}</span>;
|
||||
if (!matchingTeamMembersWithResult) return <span>{t("something_went_wrong")}</span>;
|
||||
const { result: matchingTeamMembers } = matchingTeamMembersWithResult;
|
||||
if (!matchingTeamMembers || !queryValue) {
|
||||
return (
|
||||
<div className="border-subtle bg-muted mt-4 space-y-3 rounded-md border p-4">
|
||||
<div className="text-subtle flex items-center text-sm font-medium">
|
||||
<span>{t("no_filter_set")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-subtle bg-muted mt-4 space-y-3 rounded-md border p-4">
|
||||
<div className="text-emphasis flex items-center text-sm font-medium">
|
||||
<span>{t("x_matching_members", { x: matchingTeamMembers.length })}</span>
|
||||
</div>
|
||||
<ul className="divide-subtle divide-y">
|
||||
{matchingTeamMembers.map((member) => (
|
||||
<li key={member.id} className="flex items-center py-2">
|
||||
<div className="flex flex-1 items-center space-x-2 text-sm">
|
||||
<span className="font-medium">{member.name}</span>
|
||||
<span className="text-subtle">({member.email})</span>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Segment({
|
||||
teamId,
|
||||
queryValue,
|
||||
onQueryValueChange,
|
||||
className,
|
||||
}: {
|
||||
teamId: number;
|
||||
queryValue: AttributesQueryValue | null;
|
||||
onQueryValueChange: ({ queryValue }: { queryValue: AttributesQueryValue }) => void;
|
||||
className?: string;
|
||||
}) {
|
||||
const { attributes, isPending } = useAttributes(teamId);
|
||||
const { t } = useLocale();
|
||||
if (isPending) return <span>Loading...</span>;
|
||||
if (!attributes) {
|
||||
console.log("Error fetching attributes");
|
||||
return <span>{t("something_went_wrong")}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SegmentWithAttributes
|
||||
teamId={teamId}
|
||||
attributes={attributes}
|
||||
queryValue={queryValue}
|
||||
onQueryValueChange={onQueryValueChange}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import type { Mock } from "vitest";
|
||||
|
||||
import type { AttributesQueryValue } from "@calcom/lib/raqb/types";
|
||||
import { AttributeType } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
|
||||
import { Segment } from "../Segment";
|
||||
|
||||
type Attributes = RouterOutputs["viewer"]["appRoutingForms"]["getAttributesForTeam"];
|
||||
type MatchingTeamMembersData = RouterOutputs["viewer"]["attributes"]["findTeamMembersMatchingAttributeLogic"];
|
||||
const mockGetAttributesForTeam = (
|
||||
arg: { data: Attributes; isPending: false } | { data: undefined; isPending: true }
|
||||
) => {
|
||||
(
|
||||
trpc.viewer.appRoutingForms.getAttributesForTeam.useQuery as Mock<
|
||||
typeof trpc.viewer.appRoutingForms.getAttributesForTeam.useQuery
|
||||
>
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.mockReturnValue(arg as any);
|
||||
};
|
||||
|
||||
const mockGetMatchingTeamMembers = (
|
||||
arg:
|
||||
| {
|
||||
data: MatchingTeamMembersData;
|
||||
isPending: false;
|
||||
}
|
||||
| {
|
||||
data: undefined;
|
||||
isPending: true;
|
||||
}
|
||||
) => {
|
||||
(
|
||||
trpc.viewer.attributes.findTeamMembersMatchingAttributeLogic.useQuery as Mock<
|
||||
typeof trpc.viewer.attributes.findTeamMembersMatchingAttributeLogic.useQuery
|
||||
>
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.mockReturnValue(arg as any);
|
||||
};
|
||||
|
||||
const mockAttributesWithSingleSelect = () => {
|
||||
return mockGetAttributesForTeam({
|
||||
data: [
|
||||
{
|
||||
id: "a",
|
||||
slug: "department",
|
||||
name: "Department",
|
||||
type: AttributeType.SINGLE_SELECT,
|
||||
options: [
|
||||
{ id: "1", value: "Sales", slug: "sales" },
|
||||
{ id: "2", value: "Engineering", slug: "engineering" },
|
||||
],
|
||||
},
|
||||
],
|
||||
isPending: false,
|
||||
});
|
||||
};
|
||||
|
||||
// Mock the TRPC hooks
|
||||
vi.mock("@calcom/trpc", () => ({
|
||||
trpc: {
|
||||
viewer: {
|
||||
appRoutingForms: {
|
||||
getAttributesForTeam: {
|
||||
useQuery: vi.fn(),
|
||||
},
|
||||
},
|
||||
attributes: {
|
||||
findTeamMembersMatchingAttributeLogic: {
|
||||
useQuery: vi.fn(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock useLocale hook
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
||||
useLocale: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("Segment", () => {
|
||||
const defaultProps = {
|
||||
teamId: 1,
|
||||
queryValue: null as AttributesQueryValue | null,
|
||||
onQueryValueChange: vi.fn(),
|
||||
className: "test-class",
|
||||
};
|
||||
beforeEach(() => {
|
||||
mockAttributesWithSingleSelect();
|
||||
});
|
||||
|
||||
it("renders query builder when attributes are loaded", async () => {
|
||||
mockGetMatchingTeamMembers({
|
||||
data: {
|
||||
mainWarnings: null,
|
||||
fallbackWarnings: null,
|
||||
troubleshooter: undefined,
|
||||
result: [
|
||||
{
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
isPending: false,
|
||||
});
|
||||
render(<Segment {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
// Query builder container should be present
|
||||
expect(screen.getByTestId("query-builder-container")).toBeInTheDocument();
|
||||
expect(screen.queryByText("loading")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders in loading state when matching team members are not loaded", async () => {
|
||||
mockGetMatchingTeamMembers({
|
||||
data: undefined,
|
||||
isPending: true,
|
||||
});
|
||||
render(<Segment {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("loading")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows matching team members when query value is provided", async () => {
|
||||
mockGetMatchingTeamMembers({
|
||||
isPending: false,
|
||||
data: {
|
||||
mainWarnings: null,
|
||||
fallbackWarnings: null,
|
||||
troubleshooter: undefined,
|
||||
result: [
|
||||
{
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const queryValue = {
|
||||
id: "root",
|
||||
type: "group",
|
||||
} as AttributesQueryValue;
|
||||
|
||||
render(<Segment {...defaultProps} queryValue={queryValue} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("John Doe")).toBeInTheDocument();
|
||||
expect(screen.getByText("(john@example.com)")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("shows no filter message when no query is set", async () => {
|
||||
mockGetMatchingTeamMembers({
|
||||
data: {
|
||||
mainWarnings: null,
|
||||
fallbackWarnings: null,
|
||||
troubleshooter: undefined,
|
||||
result: [] as MatchingTeamMembersData["result"],
|
||||
},
|
||||
isPending: false,
|
||||
});
|
||||
render(<Segment {...defaultProps} queryValue={null} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("no_filter_set")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -775,7 +775,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({
|
||||
>([]);
|
||||
|
||||
const findTeamMembersMatchingAttributeLogicMutation =
|
||||
trpc.viewer.routingForms.findTeamMembersMatchingAttributeLogic.useMutation({
|
||||
trpc.viewer.routingForms.findTeamMembersMatchingAttributeLogicOfRoute.useMutation({
|
||||
onSuccess(data) {
|
||||
setTeamMembersMatchingAttributeLogic(data.result ? data.result.users : data.result);
|
||||
},
|
||||
|
||||
@@ -183,7 +183,7 @@ vi.mock("@calcom/trpc/react", () => ({
|
||||
},
|
||||
},
|
||||
routingForms: {
|
||||
findTeamMembersMatchingAttributeLogic: {
|
||||
findTeamMembersMatchingAttributeLogicOfRoute: {
|
||||
useMutation: vi.fn(({ onSuccess }) => {
|
||||
return {
|
||||
mutate: vi.fn(() => {
|
||||
|
||||
@@ -30,9 +30,9 @@ function mockGetCRMContactOwnerForRRLeadSkip({
|
||||
}) {
|
||||
vi.mocked(getCRMContactOwnerForRRLeadSkip).mockImplementation((_bookerEmail, _eventMetadata) => {
|
||||
if (_bookerEmail === bookerEmail) {
|
||||
return Promise.resolve(teamMemberEmail);
|
||||
return Promise.resolve({ email: teamMemberEmail, recordType: null, crmAppSlug: null });
|
||||
}
|
||||
return Promise.resolve(undefined);
|
||||
return Promise.resolve({ email: null, recordType: null, crmAppSlug: null });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -46,9 +46,9 @@ function mockBookingFormHandler({
|
||||
vi.mocked(bookingFormHandlers.salesforce).mockImplementation(
|
||||
(_bookerEmail, _attributeRoutingConfig, _eventTypeId) => {
|
||||
if (_bookerEmail === bookerEmail) {
|
||||
return Promise.resolve({ email: teamMemberEmail });
|
||||
return Promise.resolve({ email: teamMemberEmail, recordType: null });
|
||||
}
|
||||
return Promise.resolve({ email: null });
|
||||
return Promise.resolve({ email: null, recordType: null });
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -87,8 +87,6 @@ async function createRoutingFormWithResponse({
|
||||
queryValue: {
|
||||
id: v4(),
|
||||
type: "group",
|
||||
children1: [],
|
||||
properties: {},
|
||||
},
|
||||
id: v4(),
|
||||
...route,
|
||||
@@ -173,7 +171,9 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
const ownerEmail = result.email;
|
||||
|
||||
expect(ownerEmail).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when scheduling type is not ROUND_ROBIN", async () => {
|
||||
@@ -194,7 +194,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: { ...mockEventData, schedulingType: SchedulingType.COLLECTIVE },
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(result.email).toBeNull();
|
||||
});
|
||||
|
||||
it("should return CRM owner email when valid", async () => {
|
||||
@@ -215,7 +215,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(ownerEmail);
|
||||
expect(result.email).toBe(ownerEmail);
|
||||
});
|
||||
|
||||
it("should return null when CRM owner is not found", async () => {
|
||||
@@ -228,7 +228,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(result.email).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when CRM owner is not part of event type", async () => {
|
||||
@@ -241,7 +241,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(result.email).toBeNull();
|
||||
});
|
||||
|
||||
describe("Booking form handler", () => {
|
||||
@@ -282,7 +282,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(teamMemberEmail);
|
||||
expect(result.email).toBe(teamMemberEmail);
|
||||
});
|
||||
|
||||
it("should return null when skipContactOwner is true even when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided", async () => {
|
||||
@@ -323,7 +323,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
});
|
||||
|
||||
// Because skipContactOwner is true, the booking form handler should return null
|
||||
expect(result).toBe(null);
|
||||
expect(result.email).toBe(null);
|
||||
});
|
||||
|
||||
it("should return null when when cal.routingFormResponseId and cal.salesforce.xxxx=true is provided but the returned email isn't an event member", async () => {
|
||||
@@ -364,7 +364,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(result.email).toBe(null);
|
||||
});
|
||||
|
||||
it("should return null when cal.routingFormResponseId is provided but cal.salesforce.xxxx is not", async () => {
|
||||
@@ -404,7 +404,7 @@ describe("getTeamMemberEmailForResponseOrContactUsingUrlQuery", () => {
|
||||
eventData: mockEventData,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(result.email).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ interface EventData {
|
||||
length: number;
|
||||
}
|
||||
|
||||
const returnNullValue = { email: null, recordType: null, crmAppSlug: null };
|
||||
|
||||
async function findUserByEmailWhoIsAHostOfEventType({
|
||||
email,
|
||||
eventTypeId,
|
||||
@@ -111,17 +113,20 @@ function getEnabledRoutingFormAppSlugFromQuery(query: ParsedUrlQuery) {
|
||||
/**
|
||||
* Uses the owner of the contact directly from CRM
|
||||
*/
|
||||
async function getOwnerEmailFromCrm(eventData: EventData, email: string): Promise<string | null> {
|
||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||
if (!crmContactOwnerEmail) return null;
|
||||
async function getOwnerEmailFromCrm(
|
||||
eventData: EventData,
|
||||
email: string
|
||||
): Promise<{ email: string | null; recordType: string | null; crmAppSlug: string | null }> {
|
||||
const crmContactOwner = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||
if (!crmContactOwner?.email) return returnNullValue;
|
||||
|
||||
// Determine if the contactOwner is a part of the event type
|
||||
const contactOwnerQuery = await findUserByEmailWhoIsAHostOfEventType({
|
||||
email: crmContactOwnerEmail,
|
||||
email: crmContactOwner.email,
|
||||
eventTypeId: eventData.id,
|
||||
});
|
||||
if (!contactOwnerQuery) return null;
|
||||
return crmContactOwnerEmail;
|
||||
if (!contactOwnerQuery) return returnNullValue;
|
||||
return crmContactOwner;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,7 +143,7 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
||||
attributeRoutingConfig: AttributeRoutingConfig | null;
|
||||
crmAppSlug: string;
|
||||
}) {
|
||||
const nullReturnValue = { email: null, skipContactOwner: false };
|
||||
const nullReturnValue = { email: null, skipContactOwner: false, recordType: "" };
|
||||
|
||||
if (!attributeRoutingConfig) return nullReturnValue;
|
||||
|
||||
@@ -150,7 +155,7 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
||||
const appHandler = appBookingFormHandler[crmAppSlug];
|
||||
if (!appHandler) return nullReturnValue;
|
||||
|
||||
const { email: userEmail } = await appHandler(bookerEmail, attributeRoutingConfig, eventTypeId);
|
||||
const { email: userEmail, recordType } = await appHandler(bookerEmail, attributeRoutingConfig, eventTypeId);
|
||||
|
||||
if (!userEmail) return nullReturnValue;
|
||||
|
||||
@@ -159,7 +164,7 @@ async function getTeamMemberEmailUsingRoutingFormHandler({
|
||||
|
||||
if (!userQuery) return nullReturnValue;
|
||||
|
||||
return { ...nullReturnValue, email: userEmail };
|
||||
return { ...nullReturnValue, email: userEmail, recordType };
|
||||
}
|
||||
|
||||
async function getTeamMemberEmailForResponseOrContact({
|
||||
@@ -179,7 +184,7 @@ async function getTeamMemberEmailForResponseOrContact({
|
||||
crmAppSlug?: string;
|
||||
}) {
|
||||
const eventTypeId = eventData.id;
|
||||
if (eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
||||
if (eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return returnNullValue;
|
||||
|
||||
const attributeRoutingConfigGetterData = routingFormResponseId
|
||||
? { routingFormResponseId, eventTypeId }
|
||||
@@ -194,21 +199,21 @@ async function getTeamMemberEmailForResponseOrContact({
|
||||
safeStringify({ attributeRoutingConfigGetterData, crmAppSlug })
|
||||
);
|
||||
const attributeRoutingConfig = await getAttributeRoutingConfig(attributeRoutingConfigGetterData);
|
||||
const { email, skipContactOwner } = await getTeamMemberEmailUsingRoutingFormHandler({
|
||||
const { email, skipContactOwner, recordType } = await getTeamMemberEmailUsingRoutingFormHandler({
|
||||
bookerEmail,
|
||||
eventTypeId,
|
||||
attributeRoutingConfig,
|
||||
crmAppSlug,
|
||||
});
|
||||
|
||||
if (skipContactOwner) return null;
|
||||
if (email) return email;
|
||||
if (skipContactOwner) return returnNullValue;
|
||||
if (email) return { email, recordType, crmAppSlug };
|
||||
} else {
|
||||
log.debug("Getting the contact owner email from CRM");
|
||||
return await getOwnerEmailFromCrm(eventData, bookerEmail);
|
||||
}
|
||||
|
||||
return null;
|
||||
return returnNullValue;
|
||||
}
|
||||
|
||||
export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
@@ -222,7 +227,7 @@ export async function getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
}) {
|
||||
// Without email no lookup is possible
|
||||
if (!query.email || typeof query.email !== "string") {
|
||||
return null;
|
||||
return returnNullValue;
|
||||
}
|
||||
|
||||
log.debug("getTeamMemberEmailForResponseOrContactUsingUrlQuery", safeStringify({ query }));
|
||||
|
||||
@@ -89,6 +89,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import(
|
||||
"@calcom/web/lib/getTeamMemberEmailFromCrm"
|
||||
);
|
||||
const {
|
||||
email: teamMemberEmail,
|
||||
recordType: crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
} = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query,
|
||||
eventData,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
@@ -112,10 +121,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isInstantMeeting: eventData && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
||||
teamMemberEmail: await getTeamMemberEmailForResponseOrContactUsingUrlQuery({
|
||||
query,
|
||||
eventData,
|
||||
}),
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -32,6 +32,8 @@ function Type({
|
||||
isInstantMeeting,
|
||||
orgBannerUrl,
|
||||
teamMemberEmail,
|
||||
crmOwnerRecordType,
|
||||
crmAppSlug,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -65,6 +67,8 @@ function Type({
|
||||
)}
|
||||
orgBannerUrl={orgBannerUrl}
|
||||
teamMemberEmail={teamMemberEmail}
|
||||
crmOwnerRecordType={crmOwnerRecordType}
|
||||
crmAppSlug={crmAppSlug}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -186,7 +186,7 @@ const nextConfig = {
|
||||
locales: ["en"],
|
||||
localeDetection: false,
|
||||
},
|
||||
productionBrowserSourceMaps: false,
|
||||
productionBrowserSourceMaps: process.env.SENTRY_DISABLE_CLIENT_SOURCE_MAPS === "0",
|
||||
/* We already do type check on GH actions */
|
||||
typescript: {
|
||||
ignoreBuildErrors: !!process.env.CI,
|
||||
@@ -223,6 +223,10 @@ const nextConfig = {
|
||||
},
|
||||
webpack: (config, { webpack, buildId, isServer }) => {
|
||||
if (isServer) {
|
||||
if (process.env.SENTRY_DISABLE_SERVER_SOURCE_MAPS === "1") {
|
||||
config.devtool = false;
|
||||
}
|
||||
|
||||
// Module not found fix @see https://github.com/boxyhq/jackson/issues/1535#issuecomment-1704381612
|
||||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({
|
||||
@@ -667,6 +671,10 @@ if (!!process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
||||
hideSourceMaps: true,
|
||||
// disable source map generation for the server code
|
||||
disableServerWebpackPlugin: !!process.env.SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN,
|
||||
silent: false,
|
||||
sourcemaps: {
|
||||
disable: process.env.SENTRY_DISABLE_SERVER_SOURCE_MAPS === "1",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.7.5",
|
||||
"version": "4.7.9",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "تم تحديث حدود الحجز بنجاح",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "أنت غير مخول لإجراء هذا التغيير على الحجز",
|
||||
"matching_members": "الأعضاء المطابقون",
|
||||
"x_matching_members": "{{x}} عضو مطابق",
|
||||
"matching_members_queue_using_attribute_weights": "قائمة انتظار مطابقة الأعضاء (باستخدام أوزان السمات)",
|
||||
"matching_members_queue_using_event_assignee_weights": "قائمة انتظار مطابقة الأعضاء (باستخدام أوزان المسؤولين عن الفعاليات)",
|
||||
"no_matching_members": "لا يوجد أعضاء مطابقون. سيتم الرجوع إلى استخدام أعضاء الفريق المعينين لنوع الحدث.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "هل أنت متأكد أنك تريد إعادة تعيين هذا الحجز لمضيف غير متاح؟",
|
||||
"reassign_available_team_member_description": "هل أنت متأكد أنك تريد إعادة تعيين هذا الحجز؟",
|
||||
"yes_reassign": "نعم، أعد التعيين",
|
||||
"filter_by_attributes": "تصفية حسب السمات",
|
||||
"reassign_reason": "سبب إعادة التعيين",
|
||||
"reassigned_by": "تمت إعادة التعيين بواسطة",
|
||||
"row": "صف",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "معلومات وقت انتهاء الحدث في المنطقة الزمنية للحاضر",
|
||||
"salesforce_create_event_on_contact": "إنشاء حدث على جهة الاتصال، إذا كانت موجودة. وإلا، الرجوع إلى العميل المحتمل",
|
||||
"salesforce_owner_name_to_change": "اسم المالك المراد تغييره",
|
||||
"no_filter_set": "لم يتم تعيين أي تصفية",
|
||||
"booking_dry_run_successful": "تم إجراء المحاكاة التجريبية للحجز بنجاح",
|
||||
"dont_have_access_this_page": "ليس لديك صلاحية الوصول إلى هذه الصفحة",
|
||||
"you_need_admin_or_owner_privileges_to_access": "لرؤية هذه الصفحة أو التفاعل معها، يجب أن تكون لديك صلاحيات المدير أو المالك",
|
||||
"field_type": "نوع الحقل",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "التوجيه إلى مستخدم يطابق حقل البحث في الحساب",
|
||||
"salesforce_option": "خيار Salesforce",
|
||||
"lookup_field_name": "اسم حقل البحث",
|
||||
"filter_operator_is": "يساوي",
|
||||
"filter_operator_is_not": "لا يساوي",
|
||||
"filter_operator_contains": "يحتوي على",
|
||||
"filter_operator_does_not_contain": "لا يحتوي على",
|
||||
"filter_operator_starts_with": "يبدأ بـ",
|
||||
"filter_operator_ends_with": "ينتهي بـ",
|
||||
"filter_operator_is_empty": "فارغ",
|
||||
"filter_operator_not_empty": "غير فارغ",
|
||||
"translate_description_button": "ترجمة الوصف إلى لغة متصفح الزائر باستخدام الذكاء الاصطناعي",
|
||||
"rr_distribution_method": "التوزيع",
|
||||
"rr_distribution_method_description": "يتيح تحسين التوزيع لتحقيق أقصى قدر من التوفر أو لتحقيق توزيع أكثر توازناً.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Rezervasiya məhdudiyyətləri uğurla yeniləndi",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Bu dəyişikliyi etmək üçün səlahiyyətiniz yoxdur",
|
||||
"matching_members": "Uyğun üzvlər",
|
||||
"x_matching_members": "{{x}} uyğun üzv",
|
||||
"matching_members_queue_using_attribute_weights": "Üzvlərin uyğunlaşdırılması növbəsi (atribut çəkilərindən istifadə edərək)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Üzvlərin uyğunlaşdırılması növbəsi (tədbir təyin edən çəkilərindən istifadə edərək)",
|
||||
"no_matching_members": "Uyğun üzvlər yoxdur. Bu halda tədbir növünə təyin edilmiş komanda üzvləri istifadə olunacaq.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Bu rezervasiyanı mövcud olmayan ev sahibinə yenidən təyin etmək istədiyinizə əminsiniz?",
|
||||
"reassign_available_team_member_description": "Bu rezervasiyanı yenidən təyin etmək istədiyinizə əminsiniz?",
|
||||
"yes_reassign": "Bəli, yenidən təyin et",
|
||||
"filter_by_attributes": "Atributlara görə filtrlə",
|
||||
"reassign_reason": "Yenidən təyin etmə səbəbi",
|
||||
"reassigned_by": "Yenidən təyin edən",
|
||||
"row": "sətir",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "İştirakçı Saat Qurşağında Tədbirin Bitmə Vaxtı",
|
||||
"salesforce_create_event_on_contact": "Əgər mövcuddursa, kontakt üzərində hadisə yaradın. Əks halda, lidə qayıdın",
|
||||
"salesforce_owner_name_to_change": "Dəyişdiriləcək sahibin adı",
|
||||
"no_filter_set": "Filtr təyin edilməyib",
|
||||
"booking_dry_run_successful": "Rezervasiya test yoxlaması uğurlu oldu",
|
||||
"dont_have_access_this_page": "Bu səhifəyə girişiniz yoxdur",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Bu səhifəni görmək və ya onunla qarşılıqlı əlaqədə olmaq üçün admin və ya sahib imtiyazlarına malik olmalısınız",
|
||||
"field_type": "Sahə növü",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Hesabda axtarış sahəsinə uyğun gələn istifadəçiyə yönləndir",
|
||||
"salesforce_option": "Salesforce Seçimi",
|
||||
"lookup_field_name": "Axtarış Sahəsinin Adı",
|
||||
"filter_operator_is": "Bərabərdir",
|
||||
"filter_operator_is_not": "Bərabər deyil",
|
||||
"filter_operator_contains": "Daxildir",
|
||||
"filter_operator_does_not_contain": "Daxil deyil",
|
||||
"filter_operator_starts_with": "İlə başlayır",
|
||||
"filter_operator_ends_with": "İlə bitir",
|
||||
"filter_operator_is_empty": "Boşdur",
|
||||
"filter_operator_not_empty": "Boş deyil",
|
||||
"translate_description_button": "Təsviri ziyarətçinin brauzer dilinə süni intellekt vasitəsilə tərcümə et",
|
||||
"rr_distribution_method": "Paylama",
|
||||
"rr_distribution_method_description": "Maksimum əlçatanlıq üçün paylamanı optimallaşdırmağa və ya daha balanslı bir təyinat üçün çalışmağa imkan verir.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Ограниченията за резервации са актуализирани успешно",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Нямате разрешение да правите тази промяна в резервацията",
|
||||
"matching_members": "Съвпадащи членове",
|
||||
"x_matching_members": "{{x}} съвпадащи членове",
|
||||
"matching_members_queue_using_attribute_weights": "Опашка на съвпадащи членове (използвайки тежест на атрибутите)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Опашка на съвпадащи членове (използвайки тежест на назначените за събитието)",
|
||||
"no_matching_members": "Няма съвпадащи членове. Ще се използват членовете на екипа, назначени към типа събитие.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Сигурни ли сте, че искате да преназначите тази резервация на недостъпен домакин?",
|
||||
"reassign_available_team_member_description": "Сигурни ли сте, че искате да преназначите тази резервация?",
|
||||
"yes_reassign": "Да, преназначи",
|
||||
"filter_by_attributes": "Филтриране по атрибути",
|
||||
"reassign_reason": "Причина за преназначаване",
|
||||
"reassigned_by": "Преназначено от",
|
||||
"row": "ред",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Краен час на събитието в часовата зона на участника",
|
||||
"salesforce_create_event_on_contact": "Създаване на събитие за контакт, ако съществува. В противен случай се създава за лийд",
|
||||
"salesforce_owner_name_to_change": "Име на собственик за промяна",
|
||||
"no_filter_set": "Няма зададен филтър",
|
||||
"booking_dry_run_successful": "Тестовото резервиране е успешно",
|
||||
"dont_have_access_this_page": "Нямате достъп до тази страница",
|
||||
"you_need_admin_or_owner_privileges_to_access": "За да видите или взаимодействате с тази страница, трябва да имате администраторски права или да сте собственик",
|
||||
"field_type": "Тип на полето",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Пренасочване към потребител, който съответства на поле за търсене в акаунта",
|
||||
"salesforce_option": "Salesforce опция",
|
||||
"lookup_field_name": "Име на полето за търсене",
|
||||
"filter_operator_is": "Е",
|
||||
"filter_operator_is_not": "Не е",
|
||||
"filter_operator_contains": "Съдържа",
|
||||
"filter_operator_does_not_contain": "Не съдържа",
|
||||
"filter_operator_starts_with": "Започва с",
|
||||
"filter_operator_ends_with": "Завършва с",
|
||||
"filter_operator_is_empty": "Е празно",
|
||||
"filter_operator_not_empty": "Не е празно",
|
||||
"translate_description_button": "Преведи описанието на езика на браузъра на посетителя чрез AI",
|
||||
"rr_distribution_method": "Разпределение",
|
||||
"rr_distribution_method_description": "Позволява оптимизиране на разпределението за максимална наличност или за по-балансирано разпределение.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Límits de reserva actualitzats correctament",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "No estàs autoritzat per fer aquest canvi a la reserva",
|
||||
"matching_members": "Membres coincidents",
|
||||
"x_matching_members": "{{x}} membres coincidents",
|
||||
"matching_members_queue_using_attribute_weights": "Cua de membres coincidents (utilitzant pesos d'atributs)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Cua de membres coincidents (utilitzant pesos d'assignació d'esdeveniments)",
|
||||
"no_matching_members": "No hi ha membres coincidents. Es recorrerà als membres de l'equip assignats al tipus d'esdeveniment.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Estàs segur que vols reassignar aquesta reserva a un amfitrió no disponible?",
|
||||
"reassign_available_team_member_description": "Estàs segur que vols reassignar aquesta reserva?",
|
||||
"yes_reassign": "Sí, reassignar",
|
||||
"filter_by_attributes": "Filtra per atributs",
|
||||
"reassign_reason": "Motiu de la reassignació",
|
||||
"reassigned_by": "Reassignat per",
|
||||
"row": "fila",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalització de l'esdeveniment en la zona horària de l'assistent",
|
||||
"salesforce_create_event_on_contact": "Crear esdeveniment al contacte, si existeix. Si no, utilitzar el lead",
|
||||
"salesforce_owner_name_to_change": "Nom del propietari a canviar",
|
||||
"no_filter_set": "Cap filtre establert",
|
||||
"booking_dry_run_successful": "La prova de reserva s'ha realitzat correctament",
|
||||
"dont_have_access_this_page": "No tens accés a aquesta pàgina",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Per veure o interactuar amb aquesta pàgina, has de tenir privilegis d'administrador o propietari",
|
||||
"field_type": "Tipus de camp",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Redirigir a un usuari que coincideixi amb un camp de cerca en un compte",
|
||||
"salesforce_option": "Opció de Salesforce",
|
||||
"lookup_field_name": "Nom del camp de cerca",
|
||||
"filter_operator_is": "És",
|
||||
"filter_operator_is_not": "No és",
|
||||
"filter_operator_contains": "Conté",
|
||||
"filter_operator_does_not_contain": "No conté",
|
||||
"filter_operator_starts_with": "Comença amb",
|
||||
"filter_operator_ends_with": "Acaba amb",
|
||||
"filter_operator_is_empty": "És buit",
|
||||
"filter_operator_not_empty": "No és buit",
|
||||
"translate_description_button": "Traduir la descripció a l'idioma del navegador del visitant utilitzant IA",
|
||||
"rr_distribution_method": "Distribució",
|
||||
"rr_distribution_method_description": "Permet optimitzar la distribució per maximitzar la disponibilitat o aconseguir una assignació més equilibrada",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limity rezervací byly úspěšně aktualizovány",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Nemáte oprávnění provést tuto změnu v rezervaci",
|
||||
"matching_members": "Odpovídající členové",
|
||||
"x_matching_members": "{{x}} odpovídajících členů",
|
||||
"matching_members_queue_using_attribute_weights": "Fronta přiřazování členů (podle vah atributů)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Fronta přiřazování členů (podle vah účastníků události)",
|
||||
"no_matching_members": "Žádní odpovídající členové. Bude použito záložní řešení s členy týmu přiřazenými k typu události.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Opravdu chcete přeřadit tuto rezervaci nedostupnému hostiteli?",
|
||||
"reassign_available_team_member_description": "Opravdu chcete tuto rezervaci přeřadit?",
|
||||
"yes_reassign": "Ano, přeřadit",
|
||||
"filter_by_attributes": "Filtrovat podle atributů",
|
||||
"reassign_reason": "Důvod přeřazení",
|
||||
"reassigned_by": "Přeřazeno kým",
|
||||
"row": "řádek",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Čas konce události v časovém pásmu účastníka",
|
||||
"salesforce_create_event_on_contact": "Vytvořit událost na kontaktu, pokud existuje. Jinak použít lead",
|
||||
"salesforce_owner_name_to_change": "Jméno vlastníka ke změně",
|
||||
"no_filter_set": "Není nastaven žádný filtr",
|
||||
"booking_dry_run_successful": "Testovací rezervace proběhla úspěšně",
|
||||
"dont_have_access_this_page": "K této stránce nemáte přístup",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Pro zobrazení nebo interakci s touto stránkou musíte mít administrátorská nebo vlastnická práva",
|
||||
"field_type": "Typ pole",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Přiřadit uživateli, který odpovídá vyhledávacímu poli na účtu",
|
||||
"salesforce_option": "Možnost Salesforce",
|
||||
"lookup_field_name": "Název vyhledávacího pole",
|
||||
"filter_operator_is": "Je",
|
||||
"filter_operator_is_not": "Není",
|
||||
"filter_operator_contains": "Obsahuje",
|
||||
"filter_operator_does_not_contain": "Neobsahuje",
|
||||
"filter_operator_starts_with": "Začíná na",
|
||||
"filter_operator_ends_with": "Končí na",
|
||||
"filter_operator_is_empty": "Je prázdné",
|
||||
"filter_operator_not_empty": "Není prázdné",
|
||||
"translate_description_button": "Přeložit popis do jazyka prohlížeče návštěvníka pomocí AI",
|
||||
"rr_distribution_method": "Distribuce",
|
||||
"rr_distribution_method_description": "Umožňuje optimalizovat distribuci pro maximální dostupnost nebo pro vyváženější přidělování.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Bookinggrænser opdateret med succes",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Du har ikke tilladelse til at foretage denne ændring i bookingen",
|
||||
"matching_members": "Matchende medlemmer",
|
||||
"x_matching_members": "{{x}} matchende medlemmer",
|
||||
"matching_members_queue_using_attribute_weights": "Matchende medlemskø (bruger attributvægte)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Matchende medlemskø (bruger begivenhedsansvarlig-vægte)",
|
||||
"no_matching_members": "Ingen matchende medlemmer. Det vil falde tilbage til at bruge de teammedlemmer, der er tildelt til begivenhedstypen.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Er du sikker på, at du vil omfordele denne booking til en utilgængelig vært?",
|
||||
"reassign_available_team_member_description": "Er du sikker på, at du vil omfordele denne booking?",
|
||||
"yes_reassign": "Ja, omfordel",
|
||||
"filter_by_attributes": "Filtrer efter attributter",
|
||||
"reassign_reason": "Årsag til omfordeling",
|
||||
"reassigned_by": "Omfordelt af",
|
||||
"row": "række",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Begivenhedens sluttidspunkt i deltagers tidszone",
|
||||
"salesforce_create_event_on_contact": "Opret begivenhed på kontakt, hvis den eksisterer. Ellers fald tilbage til lead",
|
||||
"salesforce_owner_name_to_change": "Ejernavn der skal ændres",
|
||||
"no_filter_set": "Intet filter indstillet",
|
||||
"booking_dry_run_successful": "Booking-testkørsel gennemført",
|
||||
"dont_have_access_this_page": "Du har ikke adgang til denne side",
|
||||
"you_need_admin_or_owner_privileges_to_access": "For at kunne se eller interagere med denne side, skal du have administrator- eller ejerrettigheder",
|
||||
"field_type": "Felttype",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Rute til en bruger, der matcher et opslagfelt på en konto",
|
||||
"salesforce_option": "Salesforce-indstilling",
|
||||
"lookup_field_name": "Opslagsfeltnavn",
|
||||
"filter_operator_is": "Er",
|
||||
"filter_operator_is_not": "Er ikke",
|
||||
"filter_operator_contains": "Indeholder",
|
||||
"filter_operator_does_not_contain": "Indeholder ikke",
|
||||
"filter_operator_starts_with": "Starter med",
|
||||
"filter_operator_ends_with": "Slutter med",
|
||||
"filter_operator_is_empty": "Er tom",
|
||||
"filter_operator_not_empty": "Er ikke tom",
|
||||
"translate_description_button": "Oversæt beskrivelsen til besøgendes browsersprog ved hjælp af AI",
|
||||
"rr_distribution_method": "Distribution",
|
||||
"rr_distribution_method_description": "Gør det muligt at optimere distributionen for maksimal tilgængelighed eller for at opnå en mere balanceret tildeling.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Buchungslimits erfolgreich aktualisiert",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Sie sind nicht berechtigt, diese Änderung an der Buchung vorzunehmen",
|
||||
"matching_members": "Passende Mitglieder",
|
||||
"x_matching_members": "{{x}} passende Mitglieder",
|
||||
"matching_members_queue_using_attribute_weights": "Warteschlange für passende Mitglieder (mit Attributgewichtung)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Warteschlange für passende Mitglieder (mit Gewichtung der Terminverantwortlichen)",
|
||||
"no_matching_members": "Keine passenden Mitglieder gefunden. Es wird auf die dem Ereignistyp zugewiesenen Teammitglieder zurückgegriffen.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Sind Sie sicher, dass Sie diese Buchung einem nicht verfügbaren Gastgeber neu zuweisen möchten?",
|
||||
"reassign_available_team_member_description": "Sind Sie sicher, dass Sie diese Buchung neu zuweisen möchten?",
|
||||
"yes_reassign": "Ja, neu zuweisen",
|
||||
"filter_by_attributes": "Nach Attributen filtern",
|
||||
"reassign_reason": "Grund für die Neuzuweisung",
|
||||
"reassigned_by": "Neu zugewiesen von",
|
||||
"row": "Zeile",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ereignis-Endzeit in der Zeitzone des Teilnehmers",
|
||||
"salesforce_create_event_on_contact": "Event für Kontakt erstellen, falls vorhanden. Andernfalls auf Lead zurückgreifen",
|
||||
"salesforce_owner_name_to_change": "Zu ändernder Besitzername",
|
||||
"no_filter_set": "Kein Filter gesetzt",
|
||||
"booking_dry_run_successful": "Buchungs-Testlauf erfolgreich",
|
||||
"dont_have_access_this_page": "Sie haben keinen Zugriff auf diese Seite",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Um diese Seite anzuzeigen oder mit ihr zu interagieren, benötigen Sie Administrator- oder Eigentümerrechte",
|
||||
"field_type": "Feldtyp",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Weiterleitung an einen Benutzer, der mit einem Lookup-Feld eines Accounts übereinstimmt",
|
||||
"salesforce_option": "Salesforce-Option",
|
||||
"lookup_field_name": "Name des Lookup-Felds",
|
||||
"filter_operator_is": "Ist",
|
||||
"filter_operator_is_not": "Ist nicht",
|
||||
"filter_operator_contains": "Enthält",
|
||||
"filter_operator_does_not_contain": "Enthält nicht",
|
||||
"filter_operator_starts_with": "Beginnt mit",
|
||||
"filter_operator_ends_with": "Endet mit",
|
||||
"filter_operator_is_empty": "Ist leer",
|
||||
"filter_operator_not_empty": "Ist nicht leer",
|
||||
"translate_description_button": "Beschreibung mit KI in die Browsersprache des Besuchers übersetzen",
|
||||
"rr_distribution_method": "Verteilung",
|
||||
"rr_distribution_method_description": "Ermöglicht die Optimierung der Verteilung für maximale Verfügbarkeit oder eine ausgewogenere Zuweisung.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Τα όρια κρατήσεων ενημερώθηκαν με επιτυχία",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Δεν έχετε εξουσιοδότηση να κάνετε αυτή την αλλαγή στην κράτηση",
|
||||
"matching_members": "Αντιστοίχιση μελών",
|
||||
"x_matching_members": "{{x}} αντίστοιχα μέλη",
|
||||
"matching_members_queue_using_attribute_weights": "Ουρά αντιστοίχισης μελών (με χρήση βαρών χαρακτηριστικών)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Ουρά αντιστοίχισης μελών (με χρήση βαρών εκχωρημένων εκδηλώσεων)",
|
||||
"no_matching_members": "Δεν βρέθηκαν αντιστοιχίσεις μελών. Θα χρησιμοποιηθούν εναλλακτικά τα μέλη ομάδας που έχουν εκχωρηθεί στον τύπο εκδήλωσης.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Είστε βέβαιοι ότι θέλετε να επαναναθέσετε αυτή την κράτηση σε μη διαθέσιμο διοργανωτή;",
|
||||
"reassign_available_team_member_description": "Είστε βέβαιοι ότι θέλετε να επαναναθέσετε αυτή την κράτηση;",
|
||||
"yes_reassign": "Ναι, επανανάθεση",
|
||||
"filter_by_attributes": "Φιλτράρισμα βάσει χαρακτηριστικών",
|
||||
"reassign_reason": "Αιτία επανανάθεσης",
|
||||
"reassigned_by": "Ανατέθηκε εκ νέου από",
|
||||
"row": "γραμμή",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ώρα λήξης εκδήλωσης στη ζώνη ώρας του συμμετέχοντα",
|
||||
"salesforce_create_event_on_contact": "Δημιουργία εκδήλωσης στην επαφή, εάν υπάρχει. Διαφορετικά, επιστροφή στο lead",
|
||||
"salesforce_owner_name_to_change": "Όνομα κατόχου προς αλλαγή",
|
||||
"no_filter_set": "Δεν έχει οριστεί φίλτρο",
|
||||
"booking_dry_run_successful": "Η δοκιμαστική κράτηση ολοκληρώθηκε με επιτυχία",
|
||||
"dont_have_access_this_page": "Δεν έχετε πρόσβαση σε αυτή τη σελίδα",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Για να δείτε ή να αλληλεπιδράσετε με αυτή τη σελίδα, πρέπει να έχετε δικαιώματα διαχειριστή ή ιδιοκτήτη",
|
||||
"field_type": "Τύπος πεδίου",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Δρομολόγηση σε χρήστη που ταιριάζει με ένα πεδίο αναζήτησης σε λογαριασμό",
|
||||
"salesforce_option": "Επιλογή Salesforce",
|
||||
"lookup_field_name": "Όνομα πεδίου αναζήτησης",
|
||||
"filter_operator_is": "Είναι",
|
||||
"filter_operator_is_not": "Δεν είναι",
|
||||
"filter_operator_contains": "Περιέχει",
|
||||
"filter_operator_does_not_contain": "Δεν περιέχει",
|
||||
"filter_operator_starts_with": "Αρχίζει με",
|
||||
"filter_operator_ends_with": "Τελειώνει με",
|
||||
"filter_operator_is_empty": "Είναι κενό",
|
||||
"filter_operator_not_empty": "Δεν είναι κενό",
|
||||
"translate_description_button": "Μετάφραση περιγραφής στη γλώσσα του προγράμματος περιήγησης του επισκέπτη με χρήση AI",
|
||||
"rr_distribution_method": "Κατανομή",
|
||||
"rr_distribution_method_description": "Επιτρέπει τη βελτιστοποίηση της κατανομής για μέγιστη διαθεσιμότητα ή για πιο ισορροπημένη ανάθεση.",
|
||||
|
||||
@@ -2682,6 +2682,7 @@
|
||||
"booking_limits_updated_successfully": "Booking limits updated successfully",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "You are unauthorized to make this change to the booking",
|
||||
"matching_members": "Matching members",
|
||||
"x_matching_members": "{{x}} matching members",
|
||||
"matching_members_queue_using_attribute_weights": "Matching members queue (using attribute weights)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Matching members queue (using event assignee weights)",
|
||||
"no_matching_members": "No matching members. It will fallback to using the team members assigned to the event type.",
|
||||
@@ -2751,6 +2752,7 @@
|
||||
"reassign_unavailable_team_member_description": "Are you sure you want to reassign this booking to an unavailable host?",
|
||||
"reassign_available_team_member_description": "Are you sure you want to reassign this booking?",
|
||||
"yes_reassign": "Yes, reassign",
|
||||
"filter_by_attributes": "Filter by attributes",
|
||||
"reassign_reason": "Reason for reassigning",
|
||||
"reassigned_by": "Reassigned by",
|
||||
"row": "row",
|
||||
@@ -2763,6 +2765,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Event End Time In Attendee Timezone",
|
||||
"salesforce_create_event_on_contact": "Create event on contact, if it exists. Else fallback to lead",
|
||||
"salesforce_owner_name_to_change": "Owner name to change",
|
||||
"no_filter_set": "No filter set",
|
||||
"booking_dry_run_successful": "Booking dry run successful",
|
||||
"dont_have_access_this_page": "You don't have access to this page",
|
||||
"you_need_admin_or_owner_privileges_to_access": "In order to view or interact with this page, you must have admin or owner privileges",
|
||||
"field_type": "Field type",
|
||||
@@ -2793,6 +2797,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
|
||||
"salesforce_option": "Salesforce Option",
|
||||
"lookup_field_name": "Lookup Field Name",
|
||||
"filter_operator_is": "Is",
|
||||
"filter_operator_is_not": "Is not",
|
||||
"filter_operator_contains": "Contains",
|
||||
"filter_operator_does_not_contain": "Does not contain",
|
||||
"filter_operator_starts_with": "Starts with",
|
||||
"filter_operator_ends_with": "Ends with",
|
||||
"filter_operator_is_empty": "Is empty",
|
||||
"filter_operator_not_empty": "Not empty",
|
||||
"translate_description_button": "Translate description to the visitor's browser language using AI",
|
||||
"rr_distribution_method": "Distribution",
|
||||
"rr_distribution_method_description": "Allows for optimising distribution for maximum availability or to aim for a more balanced assignment.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Límites de reservas actualizados con éxito",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "No tienes autorización para realizar este cambio en la reserva",
|
||||
"matching_members": "Miembros coincidentes",
|
||||
"x_matching_members": "{{x}} miembros coincidentes",
|
||||
"matching_members_queue_using_attribute_weights": "Cola de miembros coincidentes (usando pesos de atributos)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Cola de miembros coincidentes (usando pesos de asignados al evento)",
|
||||
"no_matching_members": "No hay miembros coincidentes. Se utilizarán los miembros del equipo asignados al tipo de evento como alternativa.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "¿Estás seguro de que quieres reasignar esta reserva a un anfitrión no disponible?",
|
||||
"reassign_available_team_member_description": "¿Estás seguro de que quieres reasignar esta reserva?",
|
||||
"yes_reassign": "Sí, reasignar",
|
||||
"filter_by_attributes": "Filtrar por atributos",
|
||||
"reassign_reason": "Motivo de la reasignación",
|
||||
"reassigned_by": "Reasignado por",
|
||||
"row": "fila",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalización del evento en la zona horaria del asistente",
|
||||
"salesforce_create_event_on_contact": "Crear evento en el contacto, si existe. De lo contrario, usar lead como alternativa",
|
||||
"salesforce_owner_name_to_change": "Nombre del propietario a cambiar",
|
||||
"no_filter_set": "Sin filtros establecidos",
|
||||
"booking_dry_run_successful": "Prueba de reserva exitosa",
|
||||
"dont_have_access_this_page": "No tienes acceso a esta página",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Para ver o interactuar con esta página, debes tener privilegios de administrador o propietario",
|
||||
"field_type": "Tipo de campo",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Dirigir a un usuario que coincida con un campo de búsqueda en una cuenta",
|
||||
"salesforce_option": "Opción de Salesforce",
|
||||
"lookup_field_name": "Nombre del campo de búsqueda",
|
||||
"filter_operator_is": "Es",
|
||||
"filter_operator_is_not": "No es",
|
||||
"filter_operator_contains": "Contiene",
|
||||
"filter_operator_does_not_contain": "No contiene",
|
||||
"filter_operator_starts_with": "Empieza con",
|
||||
"filter_operator_ends_with": "Termina con",
|
||||
"filter_operator_is_empty": "Está vacío",
|
||||
"filter_operator_not_empty": "No está vacío",
|
||||
"translate_description_button": "Traducir descripción al idioma del navegador del visitante usando IA",
|
||||
"rr_distribution_method": "Distribución",
|
||||
"rr_distribution_method_description": "Permite optimizar la distribución para maximizar la disponibilidad o lograr una asignación más equilibrada.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Límites de reservas actualizados con éxito",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "No tienes autorización para realizar este cambio en la reserva",
|
||||
"matching_members": "Miembros coincidentes",
|
||||
"x_matching_members": "{{x}} miembros coincidentes",
|
||||
"matching_members_queue_using_attribute_weights": "Cola de miembros coincidentes (usando pesos de atributos)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Cola de miembros coincidentes (usando pesos de asignados al evento)",
|
||||
"no_matching_members": "No hay miembros coincidentes. Se utilizarán los miembros del equipo asignados al tipo de evento como alternativa.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "¿Estás seguro de que quieres reasignar esta reserva a un anfitrión no disponible?",
|
||||
"reassign_available_team_member_description": "¿Estás seguro de que quieres reasignar esta reserva?",
|
||||
"yes_reassign": "Sí, reasignar",
|
||||
"filter_by_attributes": "Filtrar por atributos",
|
||||
"reassign_reason": "Motivo de la reasignación",
|
||||
"reassigned_by": "Reasignado por",
|
||||
"row": "fila",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalización del evento en la zona horaria del asistente",
|
||||
"salesforce_create_event_on_contact": "Crear evento en el contacto, si existe. De lo contrario, usar el lead",
|
||||
"salesforce_owner_name_to_change": "Nombre del propietario a cambiar",
|
||||
"no_filter_set": "Ningún filtro establecido",
|
||||
"booking_dry_run_successful": "Prueba de reserva exitosa",
|
||||
"dont_have_access_this_page": "No tienes acceso a esta página",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Para ver o interactuar con esta página, debes tener privilegios de administrador o propietario",
|
||||
"field_type": "Tipo de campo",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Dirigir a un usuario que coincida con un campo de búsqueda en una cuenta",
|
||||
"salesforce_option": "Opción de Salesforce",
|
||||
"lookup_field_name": "Nombre del campo de búsqueda",
|
||||
"filter_operator_is": "Es",
|
||||
"filter_operator_is_not": "No es",
|
||||
"filter_operator_contains": "Contiene",
|
||||
"filter_operator_does_not_contain": "No contiene",
|
||||
"filter_operator_starts_with": "Empieza con",
|
||||
"filter_operator_ends_with": "Termina con",
|
||||
"filter_operator_is_empty": "Está vacío",
|
||||
"filter_operator_not_empty": "No está vacío",
|
||||
"translate_description_button": "Traducir descripción al idioma del navegador del visitante usando IA",
|
||||
"rr_distribution_method": "Distribución",
|
||||
"rr_distribution_method_description": "Permite optimizar la distribución para maximizar la disponibilidad o lograr una asignación más equilibrada.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Broneerimispiirangud on edukalt uuendatud",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Teil puuduvad õigused selle broneeringu muutmiseks",
|
||||
"matching_members": "Sobivad liikmed",
|
||||
"x_matching_members": "{{x}} sobivat liiget",
|
||||
"matching_members_queue_using_attribute_weights": "Liikmete järjekord vastavalt atribuudi kaaludele",
|
||||
"matching_members_queue_using_event_assignee_weights": "Liikmete järjekord vastavalt sündmuse määratud isiku kaaludele",
|
||||
"no_matching_members": "Sobivaid liikmeid ei leitud. Kasutatakse sündmuse tüübile määratud meeskonnaliikmeid.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Kas oled kindel, et soovid selle broneeringu määrata mittesaadaolevale võõrustajale?",
|
||||
"reassign_available_team_member_description": "Kas olete kindel, et soovite selle broneeringu ümber määrata?",
|
||||
"yes_reassign": "Jah, määra ümber",
|
||||
"filter_by_attributes": "Filtreeri omaduste järgi",
|
||||
"reassign_reason": "Ümbermääramise põhjus",
|
||||
"reassigned_by": "Ümber määranud",
|
||||
"row": "rida",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Sündmuse lõpuaeg osaleja ajavööndis",
|
||||
"salesforce_create_event_on_contact": "Loo sündmus kontaktil, kui see eksisteerib. Vastasel juhul kasuta liidi",
|
||||
"salesforce_owner_name_to_change": "Muudetava omaniku nimi",
|
||||
"no_filter_set": "Filter määramata",
|
||||
"booking_dry_run_successful": "Broneeringu proovikäivitus õnnestus",
|
||||
"dont_have_access_this_page": "Teil ei ole sellele lehele juurdepääsu",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Selle lehe vaatamiseks või sellega suhtlemiseks peate omama administraatori või omaniku õigusi",
|
||||
"field_type": "Välja tüüp",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Suuna kasutajale, kes vastab konto otsinguväljale",
|
||||
"salesforce_option": "Salesforce'i valik",
|
||||
"lookup_field_name": "Otsinguvälja nimi",
|
||||
"filter_operator_is": "On",
|
||||
"filter_operator_is_not": "Ei ole",
|
||||
"filter_operator_contains": "Sisaldab",
|
||||
"filter_operator_does_not_contain": "Ei sisalda",
|
||||
"filter_operator_starts_with": "Algab",
|
||||
"filter_operator_ends_with": "Lõpeb",
|
||||
"filter_operator_is_empty": "On tühi",
|
||||
"filter_operator_not_empty": "Ei ole tühi",
|
||||
"translate_description_button": "Tõlgi kirjeldus tehisintellekti abil külastaja brauseri keelde",
|
||||
"rr_distribution_method": "Jaotamine",
|
||||
"rr_distribution_method_description": "Võimaldab optimeerida jaotust maksimaalseks kättesaadavuseks või tasakaalustatumaks määramiseks.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Erreserba-mugak ondo eguneratu dira",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Ez duzu baimenik erreserba hau aldatzeko",
|
||||
"matching_members": "Bat datozen kideak",
|
||||
"x_matching_members": "{{x}} kide bat datozenak",
|
||||
"matching_members_queue_using_attribute_weights": "Bat datozen kideen ilara (atributuen pisuak erabiliz)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Bat datozen kideen ilara (ekitaldien esleipendunen pisuak erabiliz)",
|
||||
"no_matching_members": "Ez dago bat datorren kiderik. Ekitaldi motari esleitutako taldekideak erabiliko dira ordez",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Ziur zaude erreserba hau eskuragarri ez dagoen ostalari bati esleitu nahi diozula?",
|
||||
"reassign_available_team_member_description": "Ziur zaude erreserba hau berriro esleitu nahi duzula?",
|
||||
"yes_reassign": "Bai, berriro esleitu",
|
||||
"filter_by_attributes": "Iragazi atributuen arabera",
|
||||
"reassign_reason": "Berriro esleitzeko arrazoia",
|
||||
"reassigned_by": "Honek esleitu du",
|
||||
"row": "errenkada",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Gertaeraren amaiera ordua parte-hartzailearen ordu-zonan",
|
||||
"salesforce_create_event_on_contact": "Sortu gertaera kontaktuan, existitzen bada. Bestela, lead-era itzuli",
|
||||
"salesforce_owner_name_to_change": "Aldatu beharreko jabearen izena",
|
||||
"no_filter_set": "Ez dago iragazkirik ezarrita",
|
||||
"booking_dry_run_successful": "Erreserben proba arrakastatsua",
|
||||
"dont_have_access_this_page": "Ez duzu orri honetarako sarbiderik",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Orri hau ikusteko edo erabiltzeko, administratzaile edo jabe baimenak izan behar dituzu",
|
||||
"field_type": "Eremu mota",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Kontu bateko bilaketa eremua betetzen duen erabiltzaile batengana bideratu",
|
||||
"salesforce_option": "Salesforce aukera",
|
||||
"lookup_field_name": "Bilaketa eremuaren izena",
|
||||
"filter_operator_is": "Da",
|
||||
"filter_operator_is_not": "Ez da",
|
||||
"filter_operator_contains": "Dauka",
|
||||
"filter_operator_does_not_contain": "Ez dauka",
|
||||
"filter_operator_starts_with": "Honekin hasten da",
|
||||
"filter_operator_ends_with": "Honekin amaitzen da",
|
||||
"filter_operator_is_empty": "Hutsik dago",
|
||||
"filter_operator_not_empty": "Ez dago hutsik",
|
||||
"translate_description_button": "Deskribapena bisitariaren nabigatzailearen hizkuntzara itzuli adimen artifizialaren bidez",
|
||||
"rr_distribution_method": "Banaketa",
|
||||
"rr_distribution_method_description": "Banaketa optimizatzeko aukera ematen du, erabilgarritasun maximoa edo esleipen orekatuagoa lortzeko",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limites de réservation mises à jour avec succès",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Vous n'êtes pas autorisé à apporter cette modification à la réservation",
|
||||
"matching_members": "Membres correspondants",
|
||||
"x_matching_members": "{{x}} membres correspondants",
|
||||
"matching_members_queue_using_attribute_weights": "File d'attente des membres correspondants (selon les pondérations d'attributs)",
|
||||
"matching_members_queue_using_event_assignee_weights": "File d'attente des membres correspondants (selon les pondérations des assignés à l'événement)",
|
||||
"no_matching_members": "Aucun membre correspondant. Par défaut, les membres de l'équipe assignés au type d'événement seront utilisés.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Êtes-vous sûr de vouloir réassigner cette réservation à un hôte indisponible ?",
|
||||
"reassign_available_team_member_description": "Êtes-vous sûr de vouloir réassigner cette réservation ?",
|
||||
"yes_reassign": "Oui, réassigner",
|
||||
"filter_by_attributes": "Filtrer par attributs",
|
||||
"reassign_reason": "Raison de la réassignation",
|
||||
"reassigned_by": "Réassigné par",
|
||||
"row": "ligne",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Heure de fin de l'événement dans le fuseau horaire du participant",
|
||||
"salesforce_create_event_on_contact": "Créer un événement sur le contact, s'il existe. Sinon, utiliser le lead comme alternative",
|
||||
"salesforce_owner_name_to_change": "Nom du propriétaire à modifier",
|
||||
"no_filter_set": "Aucun filtre défini",
|
||||
"booking_dry_run_successful": "Test de réservation réussi",
|
||||
"dont_have_access_this_page": "Vous n'avez pas accès à cette page",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Pour voir ou interagir avec cette page, vous devez avoir des privilèges d'administrateur ou de propriétaire",
|
||||
"field_type": "Type de champ",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Acheminer vers un utilisateur correspondant à un champ de recherche sur un compte",
|
||||
"salesforce_option": "Option Salesforce",
|
||||
"lookup_field_name": "Nom du champ de recherche",
|
||||
"filter_operator_is": "Est",
|
||||
"filter_operator_is_not": "N'est pas",
|
||||
"filter_operator_contains": "Contient",
|
||||
"filter_operator_does_not_contain": "Ne contient pas",
|
||||
"filter_operator_starts_with": "Commence par",
|
||||
"filter_operator_ends_with": "Se termine par",
|
||||
"filter_operator_is_empty": "Est vide",
|
||||
"filter_operator_not_empty": "N'est pas vide",
|
||||
"translate_description_button": "Traduire la description dans la langue du navigateur du visiteur avec l'IA",
|
||||
"rr_distribution_method": "Distribution",
|
||||
"rr_distribution_method_description": "Permet d'optimiser la distribution pour une disponibilité maximale ou pour viser une répartition plus équilibrée.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "מגבלות ההזמנה עודכנו בהצלחה",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "אין לך הרשאה לבצע שינוי זה בהזמנה",
|
||||
"matching_members": "חברים תואמים",
|
||||
"x_matching_members": "{{x}} חברים תואמים",
|
||||
"matching_members_queue_using_attribute_weights": "תור התאמת חברים (באמצעות משקלי מאפיינים)",
|
||||
"matching_members_queue_using_event_assignee_weights": "תור התאמת חברים (באמצעות משקלי מוקצים לאירוע)",
|
||||
"no_matching_members": "אין חברים תואמים. המערכת תשתמש בחברי הצוות שהוקצו לסוג האירוע.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "האם אתה בטוח שברצונך להקצות מחדש הזמנה זו למארח שאינו זמין?",
|
||||
"reassign_available_team_member_description": "האם אתה בטוח שברצונך לשייך מחדש הזמנה זו?",
|
||||
"yes_reassign": "כן, הקצה מחדש",
|
||||
"filter_by_attributes": "סינון לפי מאפיינים",
|
||||
"reassign_reason": "סיבה להקצאה מחדש",
|
||||
"reassigned_by": "הוקצה מחדש על ידי",
|
||||
"row": "שורה",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "זמן סיום האירוע באזור הזמן של המשתתף",
|
||||
"salesforce_create_event_on_contact": "צור אירוע על איש קשר, אם קיים. אחרת, חזור ללידים",
|
||||
"salesforce_owner_name_to_change": "שם הבעלים לשינוי",
|
||||
"no_filter_set": "לא הוגדר סינון",
|
||||
"booking_dry_run_successful": "הרצת הניסיון של ההזמנה הצליחה",
|
||||
"dont_have_access_this_page": "אין לך גישה לדף זה",
|
||||
"you_need_admin_or_owner_privileges_to_access": "כדי לצפות או לפעול בדף זה, עליך להיות בעל הרשאות מנהל או בעלים",
|
||||
"field_type": "סוג שדה",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "נתב למשתמש התואם לשדה חיפוש בחשבון",
|
||||
"salesforce_option": "אפשרות Salesforce",
|
||||
"lookup_field_name": "שם שדה החיפוש",
|
||||
"filter_operator_is": "הוא",
|
||||
"filter_operator_is_not": "אינו",
|
||||
"filter_operator_contains": "מכיל",
|
||||
"filter_operator_does_not_contain": "לא מכיל",
|
||||
"filter_operator_starts_with": "מתחיל ב",
|
||||
"filter_operator_ends_with": "מסתיים ב",
|
||||
"filter_operator_is_empty": "ריק",
|
||||
"filter_operator_not_empty": "לא ריק",
|
||||
"translate_description_button": "תרגם את התיאור לשפת הדפדפן של המבקר באמצעות בינה מלאכותית",
|
||||
"rr_distribution_method": "שיטת חלוקה",
|
||||
"rr_distribution_method_description": "מאפשר אופטימיזציה של החלוקה למקסימום זמינות או לחלוקה מאוזנת יותר",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "A foglalási korlátok sikeresen frissítve",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Nincs jogosultsága a foglalás módosításához",
|
||||
"matching_members": "Megfelelő tagok",
|
||||
"x_matching_members": "{{x}} egyező tag",
|
||||
"matching_members_queue_using_attribute_weights": "Tagok sorba állítása (attribútum súlyozás alapján)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Tagok sorba állítása (esemény felelős súlyozás alapján)",
|
||||
"no_matching_members": "Nincsenek megfelelő tagok. Az eseménytípushoz rendelt csapattagok kerülnek felhasználásra.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Biztosan újra szeretnéd rendelni ezt a foglalást egy nem elérhető házigazdához?",
|
||||
"reassign_available_team_member_description": "Biztosan át szeretné ütemezni ezt a foglalást?",
|
||||
"yes_reassign": "Igen, újrahozzárendelés",
|
||||
"filter_by_attributes": "Szűrés tulajdonságok szerint",
|
||||
"reassign_reason": "Újrahozzárendelés oka",
|
||||
"reassigned_by": "Újrahozzárendelő",
|
||||
"row": "sor",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Esemény befejezési ideje a résztvevő időzónájában",
|
||||
"salesforce_create_event_on_contact": "Esemény létrehozása a kapcsolaton, ha létezik. Egyébként visszalépés a leadre",
|
||||
"salesforce_owner_name_to_change": "Módosítandó tulajdonos neve",
|
||||
"no_filter_set": "Nincs beállított szűrő",
|
||||
"booking_dry_run_successful": "A foglalás tesztfuttatása sikeres",
|
||||
"dont_have_access_this_page": "Nincs hozzáférése ehhez az oldalhoz",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Az oldal megtekintéséhez vagy használatához adminisztrátori vagy tulajdonosi jogosultság szükséges",
|
||||
"field_type": "Mező típusa",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Átirányítás olyan felhasználóhoz, aki megfelel a fiók keresési mezőjének",
|
||||
"salesforce_option": "Salesforce beállítás",
|
||||
"lookup_field_name": "Keresési mező neve",
|
||||
"filter_operator_is": "Egyenlő",
|
||||
"filter_operator_is_not": "Nem egyenlő",
|
||||
"filter_operator_contains": "Tartalmazza",
|
||||
"filter_operator_does_not_contain": "Nem tartalmazza",
|
||||
"filter_operator_starts_with": "Ezzel kezdődik",
|
||||
"filter_operator_ends_with": "Ezzel végződik",
|
||||
"filter_operator_is_empty": "Üres",
|
||||
"filter_operator_not_empty": "Nem üres",
|
||||
"translate_description_button": "A leírás lefordítása a látogató böngészőjének nyelvére MI segítségével",
|
||||
"rr_distribution_method": "Elosztás",
|
||||
"rr_distribution_method_description": "Lehetővé teszi az elosztás optimalizálását a maximális rendelkezésre állás érdekében, vagy a kiegyensúlyozottabb feladatkiosztás eléréséhez.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limiti di prenotazione aggiornati con successo",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Non sei autorizzato a effettuare questa modifica alla prenotazione",
|
||||
"matching_members": "Membri corrispondenti",
|
||||
"x_matching_members": "{{x}} membri corrispondenti",
|
||||
"matching_members_queue_using_attribute_weights": "Coda di membri corrispondenti (utilizzando i pesi degli attributi)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Coda di membri corrispondenti (utilizzando i pesi degli assegnatari dell'evento)",
|
||||
"no_matching_members": "Nessun membro corrispondente. Verrà utilizzato il fallback con i membri del team assegnati al tipo di evento.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Sei sicuro di voler riassegnare questa prenotazione a un host non disponibile?",
|
||||
"reassign_available_team_member_description": "Sei sicuro di voler riassegnare questa prenotazione?",
|
||||
"yes_reassign": "Sì, riassegna",
|
||||
"filter_by_attributes": "Filtra per attributi",
|
||||
"reassign_reason": "Motivo della riassegnazione",
|
||||
"reassigned_by": "Riassegnato da",
|
||||
"row": "riga",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ora di fine evento nel fuso orario del partecipante",
|
||||
"salesforce_create_event_on_contact": "Crea evento sul contatto, se esiste. Altrimenti, passa al lead",
|
||||
"salesforce_owner_name_to_change": "Nome del proprietario da modificare",
|
||||
"no_filter_set": "Nessun filtro impostato",
|
||||
"booking_dry_run_successful": "Test di prenotazione completato con successo",
|
||||
"dont_have_access_this_page": "Non hai accesso a questa pagina",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Per visualizzare o interagire con questa pagina, devi avere privilegi di amministratore o proprietario",
|
||||
"field_type": "Tipo di campo",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Instrada a un utente che corrisponde a un campo di ricerca su un account",
|
||||
"salesforce_option": "Opzione Salesforce",
|
||||
"lookup_field_name": "Nome del campo di ricerca",
|
||||
"filter_operator_is": "È",
|
||||
"filter_operator_is_not": "Non è",
|
||||
"filter_operator_contains": "Contiene",
|
||||
"filter_operator_does_not_contain": "Non contiene",
|
||||
"filter_operator_starts_with": "Inizia con",
|
||||
"filter_operator_ends_with": "Finisce con",
|
||||
"filter_operator_is_empty": "È vuoto",
|
||||
"filter_operator_not_empty": "Non è vuoto",
|
||||
"translate_description_button": "Traduci la descrizione nella lingua del browser del visitatore usando l'IA",
|
||||
"rr_distribution_method": "Distribuzione",
|
||||
"rr_distribution_method_description": "Permette di ottimizzare la distribuzione per massimizzare la disponibilità o per ottenere un'assegnazione più bilanciata.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "予約制限が正常に更新されました",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "この予約の変更を行う権限がありません",
|
||||
"matching_members": "一致するメンバー",
|
||||
"x_matching_members": "一致するメンバー{{x}}名",
|
||||
"matching_members_queue_using_attribute_weights": "メンバーのマッチングキュー(属性の重み付けを使用)",
|
||||
"matching_members_queue_using_event_assignee_weights": "メンバーのマッチングキュー(イベント担当者の重み付けを使用)",
|
||||
"no_matching_members": "一致するメンバーがいません。イベントタイプに割り当てられたチームメンバーが使用されます。",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "利用できないホストにこの予約を再割り当てしてもよろしいですか?",
|
||||
"reassign_available_team_member_description": "この予約を再割り当てしてもよろしいですか?",
|
||||
"yes_reassign": "はい、再割り当てします",
|
||||
"filter_by_attributes": "属性でフィルター",
|
||||
"reassign_reason": "再割り当ての理由",
|
||||
"reassigned_by": "再割り当て者",
|
||||
"row": "行",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "参加者のタイムゾーンでのイベント終了時間",
|
||||
"salesforce_create_event_on_contact": "連絡先が存在する場合はイベントを作成し、存在しない場合はリードにフォールバック",
|
||||
"salesforce_owner_name_to_change": "変更する所有者名",
|
||||
"no_filter_set": "フィルター未設定",
|
||||
"booking_dry_run_successful": "予約のテストが正常に完了しました",
|
||||
"dont_have_access_this_page": "このページにアクセスする権限がありません",
|
||||
"you_need_admin_or_owner_privileges_to_access": "このページを表示または操作するには、管理者または所有者の権限が必要です",
|
||||
"field_type": "フィールドタイプ",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "取引先の参照項目に一致するユーザーに割り当てる",
|
||||
"salesforce_option": "Salesforceオプション",
|
||||
"lookup_field_name": "参照項目名",
|
||||
"filter_operator_is": "である",
|
||||
"filter_operator_is_not": "ではない",
|
||||
"filter_operator_contains": "を含む",
|
||||
"filter_operator_does_not_contain": "を含まない",
|
||||
"filter_operator_starts_with": "で始まる",
|
||||
"filter_operator_ends_with": "で終わる",
|
||||
"filter_operator_is_empty": "が空である",
|
||||
"filter_operator_not_empty": "が空ではない",
|
||||
"translate_description_button": "AIを使用して説明文を訪問者のブラウザ言語に翻訳",
|
||||
"rr_distribution_method": "配分方式",
|
||||
"rr_distribution_method_description": "最大限の予約可能性を確保するため、または均等な配分を目指すための最適化が可能です。",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "ដែនកំណត់ការកក់ត្រូវបានធ្វើបច្ចុប្បន្នភាពដោយជោគជ័យ",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "អ្នកមិនមានសិទ្ធិធ្វើការផ្លាស់ប្តូរនេះលើការកក់ទេ",
|
||||
"matching_members": "សមាជិកដែលត្រូវគ្នា",
|
||||
"x_matching_members": "សមាជិកដែលត្រូវគ្នា {{x}} នាក់",
|
||||
"matching_members_queue_using_attribute_weights": "ជួររៀងសមាជិកដែលត្រូវគ្នា (ប្រើទំងន់គុណលក្ខណៈ)",
|
||||
"matching_members_queue_using_event_assignee_weights": "ជួររៀងសមាជិកដែលត្រូវគ្នា (ប្រើទំងន់អ្នកចាត់តាំងព្រឹត្តិការណ៍)",
|
||||
"no_matching_members": "គ្មានសមាជិកដែលត្រូវគ្នា។ វានឹងត្រឡប់ទៅប្រើសមាជិកក្រុមដែលបានចាត់តាំងទៅប្រភេទព្រឹត្តិការណ៍។",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "តើអ្នកប្រាកដថាចង់កំណត់ការកក់នេះទៅម្ចាស់កម្មវិធីដែលមិនទំនេរមែនទេ?",
|
||||
"reassign_available_team_member_description": "តើអ្នកប្រាកដថាចង់ចាត់តាំងការកក់នេះឡើងវិញឬ?",
|
||||
"yes_reassign": "បាទ/ចាស កំណត់ឡើងវិញ",
|
||||
"filter_by_attributes": "ច្រោះតាមលក្ខណៈ",
|
||||
"reassign_reason": "មូលហេតុនៃការកំណត់ឡើងវិញ",
|
||||
"reassigned_by": "កំណត់ឡើងវិញដោយ",
|
||||
"row": "ជួរដេក",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "ពេលវេលាបញ្ចប់ព្រឹត្តិការណ៍នៅក្នុងតំបន់ពេលវេលារបស់អ្នកចូលរួម",
|
||||
"salesforce_create_event_on_contact": "បង្កើតព្រឹត្តិការណ៍នៅលើទំនាក់ទំនង បើមាន។ បើមិនដូច្នោះទេ ត្រឡប់ទៅ lead វិញ",
|
||||
"salesforce_owner_name_to_change": "ឈ្មោះម្ចាស់ដែលត្រូវផ្លាស់ប្តូរ",
|
||||
"no_filter_set": "មិនមានការកំណត់តម្រង",
|
||||
"booking_dry_run_successful": "ការសាកល្បងការកក់បានជោគជ័យ",
|
||||
"dont_have_access_this_page": "អ្នកមិនមានសិទ្ធិចូលប្រើទំព័រនេះទេ",
|
||||
"you_need_admin_or_owner_privileges_to_access": "ដើម្បីមើល ឬប្រើប្រាស់ទំព័រនេះ អ្នកត្រូវតែមានសិទ្ធិជាអ្នកគ្រប់គ្រង ឬម្ចាស់",
|
||||
"field_type": "ប្រភេទវាល",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "ផ្លូវទៅកាន់អ្នកប្រើដែលត្រូវនឹងវាលស្វែងរកនៅលើគណនី។",
|
||||
"salesforce_option": "ជម្រើស Salesforce",
|
||||
"lookup_field_name": "ឈ្មោះវាលស្វែងរក",
|
||||
"filter_operator_is": "គឺ",
|
||||
"filter_operator_is_not": "មិនមែនជា",
|
||||
"filter_operator_contains": "មាន",
|
||||
"filter_operator_does_not_contain": "មិនមាន",
|
||||
"filter_operator_starts_with": "ចាប់ផ្តើមដោយ",
|
||||
"filter_operator_ends_with": "បញ្ចប់ដោយ",
|
||||
"filter_operator_is_empty": "ទទេ",
|
||||
"filter_operator_not_empty": "មិនទទេ",
|
||||
"translate_description_button": "បកប្រែការពិពណ៌នាទៅជាភាសាកម្មវិធីរុករករបស់ភ្ញៀវដោយប្រើ AI",
|
||||
"rr_distribution_method": "ការចែកចាយ",
|
||||
"rr_distribution_method_description": "អនុញ្ញាតឱ្យធ្វើការបែងចែកប្រសិទ្ធភាពដើម្បីបង្កើនភាពអាចរកបានអតិបរមា ឬដើម្បីសម្រេចបាននូវការចាត់តាំងដែលមានតុល្យភាពជាងមុន។",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "예약 제한이 성공적으로 업데이트되었습니다",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "이 예약을 변경할 권한이 없습니다",
|
||||
"matching_members": "일치하는 멤버",
|
||||
"x_matching_members": "일치하는 멤버 {{x}}명",
|
||||
"matching_members_queue_using_attribute_weights": "속성 가중치를 사용한 멤버 매칭 대기열",
|
||||
"matching_members_queue_using_event_assignee_weights": "이벤트 담당자 가중치를 사용한 멤버 매칭 대기열",
|
||||
"no_matching_members": "일치하는 멤버가 없습니다. 이벤트 유형에 할당된 팀 멤버를 사용할 것입니다.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "이 예약을 불가능한 호스트에게 재배정하시겠습니까?",
|
||||
"reassign_available_team_member_description": "이 예약을 재할당하시겠습니까?",
|
||||
"yes_reassign": "네, 재배정합니다",
|
||||
"filter_by_attributes": "속성으로 필터링",
|
||||
"reassign_reason": "재배정 이유",
|
||||
"reassigned_by": "재배정한 사람",
|
||||
"row": "행",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "참석자 시간대의 이벤트 종료 시간",
|
||||
"salesforce_create_event_on_contact": "연락처가 존재하는 경우 이벤트 생성. 그렇지 않으면 리드로 대체",
|
||||
"salesforce_owner_name_to_change": "변경할 소유자 이름",
|
||||
"no_filter_set": "설정된 필터 없음",
|
||||
"booking_dry_run_successful": "예약 테스트 실행 성공",
|
||||
"dont_have_access_this_page": "이 페이지에 접근할 수 없습니다",
|
||||
"you_need_admin_or_owner_privileges_to_access": "이 페이지를 보거나 상호작용하려면 관리자 또는 소유자 권한이 필요합니다",
|
||||
"field_type": "필드 유형",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "계정의 조회 필드와 일치하는 사용자에게 라우팅합니다.",
|
||||
"salesforce_option": "Salesforce 옵션",
|
||||
"lookup_field_name": "조회 필드 이름",
|
||||
"filter_operator_is": "일치",
|
||||
"filter_operator_is_not": "불일치",
|
||||
"filter_operator_contains": "포함",
|
||||
"filter_operator_does_not_contain": "미포함",
|
||||
"filter_operator_starts_with": "다음으로 시작",
|
||||
"filter_operator_ends_with": "다음으로 끝남",
|
||||
"filter_operator_is_empty": "비어 있음",
|
||||
"filter_operator_not_empty": "비어 있지 않음",
|
||||
"translate_description_button": "AI를 사용하여 방문자의 브라우저 언어로 설명 번역하기",
|
||||
"rr_distribution_method": "분배 방식",
|
||||
"rr_distribution_method_description": "최대 가용성을 위한 분배 최적화 또는 균형 잡힌 할당을 위한 설정이 가능합니다.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Boekingslimieten succesvol bijgewerkt",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "U bent niet gemachtigd om deze wijziging in de boeking aan te brengen",
|
||||
"matching_members": "Overeenkomende leden",
|
||||
"x_matching_members": "{{x}} overeenkomende leden",
|
||||
"matching_members_queue_using_attribute_weights": "Wachtrij voor overeenkomende leden (op basis van kenmerkengewicht)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Wachtrij voor overeenkomende leden (op basis van toewijzingsgewicht)",
|
||||
"no_matching_members": "Geen overeenkomende leden. Er wordt teruggevallen op de teamleden die aan het type evenement zijn toegewezen.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Weet je zeker dat je deze boeking wilt toewijzen aan een niet-beschikbare host?",
|
||||
"reassign_available_team_member_description": "Weet je zeker dat je deze boeking opnieuw wilt toewijzen?",
|
||||
"yes_reassign": "Ja, opnieuw toewijzen",
|
||||
"filter_by_attributes": "Filteren op kenmerken",
|
||||
"reassign_reason": "Reden voor opnieuw toewijzen",
|
||||
"reassigned_by": "Opnieuw toegewezen door",
|
||||
"row": "rij",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Eindtijd evenement in tijdzone deelnemer",
|
||||
"salesforce_create_event_on_contact": "Maak een evenement aan op het contact, indien aanwezig. Anders terugvallen op lead",
|
||||
"salesforce_owner_name_to_change": "Eigenaarsnaam om te wijzigen",
|
||||
"no_filter_set": "Geen filter ingesteld",
|
||||
"booking_dry_run_successful": "Boeking test succesvol",
|
||||
"dont_have_access_this_page": "Je hebt geen toegang tot deze pagina",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Om deze pagina te bekijken of ermee te communiceren, moet je over admin- of eigenaarsrechten beschikken",
|
||||
"field_type": "Veldtype",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Routeer naar een gebruiker die overeenkomt met een zoekveld op een account",
|
||||
"salesforce_option": "Salesforce-optie",
|
||||
"lookup_field_name": "Zoekveldnaam",
|
||||
"filter_operator_is": "Is",
|
||||
"filter_operator_is_not": "Is niet",
|
||||
"filter_operator_contains": "Bevat",
|
||||
"filter_operator_does_not_contain": "Bevat niet",
|
||||
"filter_operator_starts_with": "Begint met",
|
||||
"filter_operator_ends_with": "Eindigt met",
|
||||
"filter_operator_is_empty": "Is leeg",
|
||||
"filter_operator_not_empty": "Is niet leeg",
|
||||
"translate_description_button": "Vertaal beschrijving naar de browsertaal van de bezoeker met behulp van AI",
|
||||
"rr_distribution_method": "Distributie",
|
||||
"rr_distribution_method_description": "Maakt het mogelijk om de distributie te optimaliseren voor maximale beschikbaarheid of voor een meer gebalanceerde toewijzing.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Bestillingsgrenser oppdatert vellykket",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Du har ikke tillatelse til å gjøre denne endringen i bestillingen",
|
||||
"matching_members": "Matchende medlemmer",
|
||||
"x_matching_members": "{{x}} matchende medlemmer",
|
||||
"matching_members_queue_using_attribute_weights": "Kø for samsvarende medlemmer (bruker attributtvekting)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Kø for samsvarende medlemmer (bruker vekting for hendelsesansvarlige)",
|
||||
"no_matching_members": "Ingen matchende medlemmer. Det vil falle tilbake til å bruke teammedlemmene som er tildelt hendelsestypen.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Er du sikker på at du vil omfordele denne bookingen til en utilgjengelig vert?",
|
||||
"reassign_available_team_member_description": "Er du sikker på at du vil tildele denne bookingen på nytt?",
|
||||
"yes_reassign": "Ja, omfordel",
|
||||
"filter_by_attributes": "Filtrer etter attributter",
|
||||
"reassign_reason": "Årsak til omfordeling",
|
||||
"reassigned_by": "Omfordelt av",
|
||||
"row": "rad",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hendelsens sluttid i deltakers tidssone",
|
||||
"salesforce_create_event_on_contact": "Opprett hendelse på kontakt, hvis den eksisterer. Ellers faller tilbake til lead",
|
||||
"salesforce_owner_name_to_change": "Eiernavn som skal endres",
|
||||
"no_filter_set": "Ingen filter er satt",
|
||||
"booking_dry_run_successful": "Test av booking var vellykket",
|
||||
"dont_have_access_this_page": "Du har ikke tilgang til denne siden",
|
||||
"you_need_admin_or_owner_privileges_to_access": "For å se eller bruke denne siden, må du ha administrator- eller eierrettigheter",
|
||||
"field_type": "Felttype",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Rute til en bruker som samsvarer med et oppslagsfelt på en konto",
|
||||
"salesforce_option": "Salesforce-alternativ",
|
||||
"lookup_field_name": "Oppslagsfeltnavn",
|
||||
"filter_operator_is": "Er",
|
||||
"filter_operator_is_not": "Er ikke",
|
||||
"filter_operator_contains": "Inneholder",
|
||||
"filter_operator_does_not_contain": "Inneholder ikke",
|
||||
"filter_operator_starts_with": "Starter med",
|
||||
"filter_operator_ends_with": "Slutter med",
|
||||
"filter_operator_is_empty": "Er tom",
|
||||
"filter_operator_not_empty": "Er ikke tom",
|
||||
"translate_description_button": "Oversett beskrivelsen til besøkendes nettleserspråk ved hjelp av AI",
|
||||
"rr_distribution_method": "Distribusjon",
|
||||
"rr_distribution_method_description": "Gjør det mulig å optimalisere distribusjonen for maksimal tilgjengelighet eller for å oppnå en mer balansert fordeling.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limity rezerwacji zostały pomyślnie zaktualizowane",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Nie masz uprawnień do wprowadzenia tej zmiany w rezerwacji",
|
||||
"matching_members": "Pasujący członkowie",
|
||||
"x_matching_members": "{{x}} pasujących członków",
|
||||
"matching_members_queue_using_attribute_weights": "Kolejka dopasowanych członków (używając wag atrybutów)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Kolejka dopasowanych członków (używając wag przypisanych do wydarzeń)",
|
||||
"no_matching_members": "Brak pasujących członków. Zostaną użyci członkowie zespołu przypisani do typu wydarzenia.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Czy na pewno chcesz przypisać tę rezerwację do niedostępnego hosta?",
|
||||
"reassign_available_team_member_description": "Czy na pewno chcesz przypisać tę rezerwację ponownie?",
|
||||
"yes_reassign": "Tak, przypisz ponownie",
|
||||
"filter_by_attributes": "Filtruj według atrybutów",
|
||||
"reassign_reason": "Powód ponownego przypisania",
|
||||
"reassigned_by": "Przypisane ponownie przez",
|
||||
"row": "wiersz",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Czas zakończenia wydarzenia w strefie czasowej uczestnika",
|
||||
"salesforce_create_event_on_contact": "Utwórz wydarzenie na kontakcie, jeśli istnieje. W przeciwnym razie użyj leada",
|
||||
"salesforce_owner_name_to_change": "Nazwa właściciela do zmiany",
|
||||
"no_filter_set": "Brak ustawionego filtru",
|
||||
"booking_dry_run_successful": "Test rezerwacji zakończony pomyślnie",
|
||||
"dont_have_access_this_page": "Nie masz dostępu do tej strony",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Aby wyświetlić lub korzystać z tej strony, musisz mieć uprawnienia administratora lub właściciela",
|
||||
"field_type": "Typ pola",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Przekieruj do użytkownika, który odpowiada polu wyszukiwania w koncie",
|
||||
"salesforce_option": "Opcja Salesforce",
|
||||
"lookup_field_name": "Nazwa pola wyszukiwania",
|
||||
"filter_operator_is": "Jest",
|
||||
"filter_operator_is_not": "Nie jest",
|
||||
"filter_operator_contains": "Zawiera",
|
||||
"filter_operator_does_not_contain": "Nie zawiera",
|
||||
"filter_operator_starts_with": "Zaczyna się od",
|
||||
"filter_operator_ends_with": "Kończy się na",
|
||||
"filter_operator_is_empty": "Jest pusty",
|
||||
"filter_operator_not_empty": "Nie jest pusty",
|
||||
"translate_description_button": "Przetłumacz opis na język przeglądarki użytkownika za pomocą AI",
|
||||
"rr_distribution_method": "Dystrybucja",
|
||||
"rr_distribution_method_description": "Pozwala zoptymalizować dystrybucję pod kątem maksymalnej dostępności lub bardziej zrównoważonego przydziału spotkań.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limites de agendamento atualizados com sucesso",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Você não tem autorização para fazer essa alteração na reserva",
|
||||
"matching_members": "Membros correspondentes",
|
||||
"x_matching_members": "{{x}} membros correspondentes",
|
||||
"matching_members_queue_using_attribute_weights": "Fila de membros correspondentes (usando pesos de atributos)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Fila de membros correspondentes (usando pesos de designados do evento)",
|
||||
"no_matching_members": "Nenhum membro correspondente. Será utilizado como alternativa os membros da equipe atribuídos ao tipo de evento.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Tem certeza de que deseja reatribuir esta reserva a um anfitrião indisponível?",
|
||||
"reassign_available_team_member_description": "Tem certeza de que deseja reatribuir esta reserva?",
|
||||
"yes_reassign": "Sim, reatribuir",
|
||||
"filter_by_attributes": "Filtrar por atributos",
|
||||
"reassign_reason": "Motivo da reatribuição",
|
||||
"reassigned_by": "Reatribuído por",
|
||||
"row": "linha",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de término do evento no fuso horário do participante",
|
||||
"salesforce_create_event_on_contact": "Criar evento no contato, se existir. Caso contrário, usar lead como alternativa",
|
||||
"salesforce_owner_name_to_change": "Nome do proprietário a ser alterado",
|
||||
"no_filter_set": "Nenhum filtro definido",
|
||||
"booking_dry_run_successful": "Simulação de agendamento bem-sucedida",
|
||||
"dont_have_access_this_page": "Você não tem acesso a esta página",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Para visualizar ou interagir com esta página, você deve ter privilégios de administrador ou proprietário",
|
||||
"field_type": "Tipo de campo",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Direcionar para um usuário que corresponda a um campo de pesquisa em uma conta",
|
||||
"salesforce_option": "Opção do Salesforce",
|
||||
"lookup_field_name": "Nome do campo de pesquisa",
|
||||
"filter_operator_is": "É",
|
||||
"filter_operator_is_not": "Não é",
|
||||
"filter_operator_contains": "Contém",
|
||||
"filter_operator_does_not_contain": "Não contém",
|
||||
"filter_operator_starts_with": "Começa com",
|
||||
"filter_operator_ends_with": "Termina com",
|
||||
"filter_operator_is_empty": "Está vazio",
|
||||
"filter_operator_not_empty": "Não está vazio",
|
||||
"translate_description_button": "Traduzir descrição para o idioma do navegador do visitante usando IA",
|
||||
"rr_distribution_method": "Distribuição",
|
||||
"rr_distribution_method_description": "Permite otimizar a distribuição para máxima disponibilidade ou buscar uma atribuição mais equilibrada.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limites de reservas atualizados com sucesso",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Não tem autorização para fazer esta alteração na reserva",
|
||||
"matching_members": "Membros correspondentes",
|
||||
"x_matching_members": "{{x}} membros correspondentes",
|
||||
"matching_members_queue_using_attribute_weights": "Fila de membros correspondentes (usando pesos de atributos)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Fila de membros correspondentes (usando pesos de responsáveis por eventos)",
|
||||
"no_matching_members": "Nenhum membro correspondente. Será utilizado como alternativa os membros da equipa atribuídos ao tipo de evento.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Tem a certeza de que quer reatribuir esta reserva a um anfitrião indisponível?",
|
||||
"reassign_available_team_member_description": "Tem a certeza de que deseja reatribuir esta marcação?",
|
||||
"yes_reassign": "Sim, reatribuir",
|
||||
"filter_by_attributes": "Filtrar por atributos",
|
||||
"reassign_reason": "Motivo da reatribuição",
|
||||
"reassigned_by": "Reatribuído por",
|
||||
"row": "linha",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de fim do evento no fuso horário do participante",
|
||||
"salesforce_create_event_on_contact": "Criar evento no contacto, se existir. Caso contrário, recorrer ao lead",
|
||||
"salesforce_owner_name_to_change": "Nome do proprietário a alterar",
|
||||
"no_filter_set": "Nenhum filtro definido",
|
||||
"booking_dry_run_successful": "Teste de agendamento realizado com sucesso",
|
||||
"dont_have_access_this_page": "Você não tem acesso a esta página",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Para visualizar ou interagir com esta página, você deve ter privilégios de administrador ou proprietário",
|
||||
"field_type": "Tipo de campo",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Direcionar para um usuário que corresponda a um campo de pesquisa em uma conta",
|
||||
"salesforce_option": "Opção do Salesforce",
|
||||
"lookup_field_name": "Nome do campo de pesquisa",
|
||||
"filter_operator_is": "É",
|
||||
"filter_operator_is_not": "Não é",
|
||||
"filter_operator_contains": "Contém",
|
||||
"filter_operator_does_not_contain": "Não contém",
|
||||
"filter_operator_starts_with": "Começa com",
|
||||
"filter_operator_ends_with": "Termina com",
|
||||
"filter_operator_is_empty": "Está vazio",
|
||||
"filter_operator_not_empty": "Não está vazio",
|
||||
"translate_description_button": "Traduzir descrição para o idioma do navegador do visitante usando IA",
|
||||
"rr_distribution_method": "Distribuição",
|
||||
"rr_distribution_method_description": "Permite otimizar a distribuição para máxima disponibilidade ou para buscar uma atribuição mais equilibrada.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limitele de rezervare au fost actualizate cu succes",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Nu aveți autorizația necesară pentru a face această modificare în rezervare",
|
||||
"matching_members": "Membri corespunzători",
|
||||
"x_matching_members": "{{x}} membri potriviți",
|
||||
"matching_members_queue_using_attribute_weights": "Coadă de potrivire membri (folosind ponderi de atribute)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Coadă de potrivire membri (folosind ponderi pentru responsabilii evenimentelor)",
|
||||
"no_matching_members": "Nu există membri corespunzători. Se va reveni la utilizarea membrilor echipei alocați tipului de eveniment.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Ești sigur că vrei să reatribui această rezervare unei gazde indisponibile?",
|
||||
"reassign_available_team_member_description": "Sunteți sigur că doriți să reatribuiți această rezervare?",
|
||||
"yes_reassign": "Da, reatribuie",
|
||||
"filter_by_attributes": "Filtrează după atribute",
|
||||
"reassign_reason": "Motivul reatribuirii",
|
||||
"reassigned_by": "Reatribuit de",
|
||||
"row": "rând",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ora de sfârșit a evenimentului în fusul orar al participantului",
|
||||
"salesforce_create_event_on_contact": "Creați evenimentul pe contact, dacă există. Altfel, reveniți la lead",
|
||||
"salesforce_owner_name_to_change": "Numele proprietarului de schimbat",
|
||||
"no_filter_set": "Niciun filtru setat",
|
||||
"booking_dry_run_successful": "Simulare programare reușită",
|
||||
"dont_have_access_this_page": "Nu aveți acces la această pagină",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Pentru a vizualiza sau interacționa cu această pagină, trebuie să aveți privilegii de administrator sau proprietar",
|
||||
"field_type": "Tip câmp",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Redirecționează către un utilizator care se potrivește cu un câmp de căutare pe un cont",
|
||||
"salesforce_option": "Opțiune Salesforce",
|
||||
"lookup_field_name": "Nume câmp de căutare",
|
||||
"filter_operator_is": "Este",
|
||||
"filter_operator_is_not": "Nu este",
|
||||
"filter_operator_contains": "Conține",
|
||||
"filter_operator_does_not_contain": "Nu conține",
|
||||
"filter_operator_starts_with": "Începe cu",
|
||||
"filter_operator_ends_with": "Se termină cu",
|
||||
"filter_operator_is_empty": "Este gol",
|
||||
"filter_operator_not_empty": "Nu este gol",
|
||||
"translate_description_button": "Traduce descrierea în limba browserului vizitatorului folosind AI",
|
||||
"rr_distribution_method": "Distribuție",
|
||||
"rr_distribution_method_description": "Permite optimizarea distribuției pentru disponibilitate maximă sau pentru o alocare mai echilibrată.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Ограничения бронирования успешно обновлены",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "У вас нет прав для внесения этих изменений в бронирование",
|
||||
"matching_members": "Подходящие участники",
|
||||
"x_matching_members": "{{x}} подходящих участников",
|
||||
"matching_members_queue_using_attribute_weights": "Очередь подходящих участников (с использованием весов атрибутов)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Очередь подходящих участников (с использованием весов назначенных на событие)",
|
||||
"no_matching_members": "Нет подходящих участников. Будут использованы участники команды, назначенные на тип события.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Вы уверены, что хотите переназначить эту встречу недоступному организатору?",
|
||||
"reassign_available_team_member_description": "Вы уверены, что хотите переназначить эту встречу?",
|
||||
"yes_reassign": "Да, переназначить",
|
||||
"filter_by_attributes": "Фильтровать по атрибутам",
|
||||
"reassign_reason": "Причина переназначения",
|
||||
"reassigned_by": "Переназначено",
|
||||
"row": "строка",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Время окончания события в часовом поясе участника",
|
||||
"salesforce_create_event_on_contact": "Создать событие для контакта, если он существует. В противном случае создать для лида",
|
||||
"salesforce_owner_name_to_change": "Имя владельца для изменения",
|
||||
"no_filter_set": "Фильтр не установлен",
|
||||
"booking_dry_run_successful": "Тестовое бронирование выполнено успешно",
|
||||
"dont_have_access_this_page": "У вас нет доступа к этой странице",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Для просмотра или взаимодействия с этой страницей необходимы права администратора или владельца",
|
||||
"field_type": "Тип поля",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Направить пользователю, соответствующему полю поиска в аккаунте",
|
||||
"salesforce_option": "Опция Salesforce",
|
||||
"lookup_field_name": "Название поля поиска",
|
||||
"filter_operator_is": "Равно",
|
||||
"filter_operator_is_not": "Не равно",
|
||||
"filter_operator_contains": "Содержит",
|
||||
"filter_operator_does_not_contain": "Не содержит",
|
||||
"filter_operator_starts_with": "Начинается с",
|
||||
"filter_operator_ends_with": "Заканчивается на",
|
||||
"filter_operator_is_empty": "Пусто",
|
||||
"filter_operator_not_empty": "Не пусто",
|
||||
"translate_description_button": "Перевести описание на язык браузера посетителя с помощью ИИ",
|
||||
"rr_distribution_method": "Распределение",
|
||||
"rr_distribution_method_description": "Позволяет оптимизировать распределение для максимальной доступности или для достижения более сбалансированного назначения",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Limity rezervácií boli úspešne aktualizované",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Nemáte oprávnenie vykonať túto zmenu v rezervácii",
|
||||
"matching_members": "Zodpovedajúci členovia",
|
||||
"x_matching_members": "{{x}} zodpovedajúcich členov",
|
||||
"matching_members_queue_using_attribute_weights": "Poradie členov podľa zhody (použitie váh atribútov)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Poradie členov podľa zhody (použitie váh priradenia udalosti)",
|
||||
"no_matching_members": "Žiadni zodpovedajúci členovia. Použijú sa členovia tímu priradení k typu udalosti.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Ste si istí, že chcete priradiť túto rezerváciu k nedostupnému hostiteľovi?",
|
||||
"reassign_available_team_member_description": "Ste si istí, že chcete priradiť túto rezerváciu?",
|
||||
"yes_reassign": "Áno, priradiť",
|
||||
"filter_by_attributes": "Filtrovať podľa atribútov",
|
||||
"reassign_reason": "Dôvod priradenia",
|
||||
"reassigned_by": "Priradené od",
|
||||
"row": "riadok",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Čas konca udalosti v časovej zóne účastníka",
|
||||
"salesforce_create_event_on_contact": "Vytvoriť udalosť na kontakte, ak existuje. Inak prepnúť na potenciálneho zákazníka",
|
||||
"salesforce_owner_name_to_change": "Meno vlastníka na zmenu",
|
||||
"no_filter_set": "Nie je nastavený žiadny filter",
|
||||
"booking_dry_run_successful": "Testovacia rezervácia prebehla úspešne",
|
||||
"dont_have_access_this_page": "Nemáte prístup na túto stránku",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Na zobrazenie alebo interakciu s touto stránkou musíte mať administrátorské alebo vlastnícke práva",
|
||||
"field_type": "Typ poľa",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Smerovať na používateľa, ktorý zodpovedá poľu vyhľadávania na účte",
|
||||
"salesforce_option": "Možnosť Salesforce",
|
||||
"lookup_field_name": "Názov poľa vyhľadávania",
|
||||
"filter_operator_is": "Je",
|
||||
"filter_operator_is_not": "Nie je",
|
||||
"filter_operator_contains": "Obsahuje",
|
||||
"filter_operator_does_not_contain": "Neobsahuje",
|
||||
"filter_operator_starts_with": "Začína na",
|
||||
"filter_operator_ends_with": "Končí na",
|
||||
"filter_operator_is_empty": "Je prázdne",
|
||||
"filter_operator_not_empty": "Nie je prázdne",
|
||||
"translate_description_button": "Preložiť popis do jazyka prehliadača návštevníka pomocou AI",
|
||||
"rr_distribution_method": "Distribúcia",
|
||||
"rr_distribution_method_description": "Umožňuje optimalizovať distribúciu pre maximálnu dostupnosť alebo pre vyváženejšie prideľovanie.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Ograničenja zakazivanja su uspešno ažurirana",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Nemate ovlašćenje da izvršite ovu promenu u rezervaciji",
|
||||
"matching_members": "Одговарајући чланови",
|
||||
"x_matching_members": "{{x}} članova koji odgovaraju",
|
||||
"matching_members_queue_using_attribute_weights": "Red članova za uparivanje (korišćenjem težinskih atributa)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Red članova za uparivanje (korišćenjem težinskih vrednosti dodeljenih događaju)",
|
||||
"no_matching_members": "Нема одговарајућих чланова. Систем ће се вратити на коришћење чланова тима додељених типу догађаја.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Da li ste sigurni da želite da preraspodelite ovu rezervaciju nedostupnom domaćinu?",
|
||||
"reassign_available_team_member_description": "Da li ste sigurni da želite da preraspodelite ovu rezervaciju?",
|
||||
"yes_reassign": "Da, preraspodelite",
|
||||
"filter_by_attributes": "Filtriraj po atributima",
|
||||
"reassign_reason": "Razlog za preraspodelu",
|
||||
"reassigned_by": "Preraspodelilo",
|
||||
"row": "red",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Vreme završetka događaja u vremenskoj zoni učesnika",
|
||||
"salesforce_create_event_on_contact": "Kreiraj događaj na kontaktu, ako postoji. U suprotnom, prebaci na lead",
|
||||
"salesforce_owner_name_to_change": "Ime vlasnika koje treba promeniti",
|
||||
"no_filter_set": "Nije postavljen filter",
|
||||
"booking_dry_run_successful": "Test rezervacije uspešno završen",
|
||||
"dont_have_access_this_page": "Nemate pristup ovoj stranici",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Da biste pregledali ili koristili ovu stranicu, morate imati administratorske ili vlasničke privilegije",
|
||||
"field_type": "Tip polja",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Usmeri ka korisniku koji odgovara polju za pretragu na nalogu",
|
||||
"salesforce_option": "Salesforce opcija",
|
||||
"lookup_field_name": "Naziv polja za pretragu",
|
||||
"filter_operator_is": "Je",
|
||||
"filter_operator_is_not": "Nije",
|
||||
"filter_operator_contains": "Sadrži",
|
||||
"filter_operator_does_not_contain": "Ne sadrži",
|
||||
"filter_operator_starts_with": "Počinje sa",
|
||||
"filter_operator_ends_with": "Završava sa",
|
||||
"filter_operator_is_empty": "Prazno je",
|
||||
"filter_operator_not_empty": "Nije prazno",
|
||||
"translate_description_button": "Prevedi opis na jezik posetiočevog pretraživača koristeći AI",
|
||||
"rr_distribution_method": "Distribucija",
|
||||
"rr_distribution_method_description": "Omogućava optimizaciju distribucije za maksimalnu dostupnost ili za balansiranu raspodelu.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Bokningsgränser uppdaterades framgångsrikt",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Du har inte behörighet att göra denna ändring i bokningen",
|
||||
"matching_members": "Matchande medlemmar",
|
||||
"x_matching_members": "{{x}} matchande medlemmar",
|
||||
"matching_members_queue_using_attribute_weights": "Matchande medlemmars kö (med attributvikter)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Matchande medlemmars kö (med händelsetilldelningsvikter)",
|
||||
"no_matching_members": "Inga matchande medlemmar. Det kommer att återgå till att använda de teammedlemmar som är tilldelade till händelsetypen.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Är du säker på att du vill omfördela denna bokning till en otillgänglig värd?",
|
||||
"reassign_available_team_member_description": "Är du säker på att du vill omfördela denna bokning?",
|
||||
"yes_reassign": "Ja, omfördela",
|
||||
"filter_by_attributes": "Filtrera efter attribut",
|
||||
"reassign_reason": "Anledning till omfördelning",
|
||||
"reassigned_by": "Omfördelad av",
|
||||
"row": "rad",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Händelsens sluttid i deltagarens tidszon",
|
||||
"salesforce_create_event_on_contact": "Skapa händelse på kontakt om den finns, annars fallback till lead",
|
||||
"salesforce_owner_name_to_change": "Ägarnamn att ändra",
|
||||
"no_filter_set": "Inget filter inställt",
|
||||
"booking_dry_run_successful": "Testbokning genomförd",
|
||||
"dont_have_access_this_page": "Du har inte åtkomst till den här sidan",
|
||||
"you_need_admin_or_owner_privileges_to_access": "För att visa eller interagera med den här sidan måste du ha administratörs- eller ägarprivilegier",
|
||||
"field_type": "Fälttyp",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Rikta till en användare som matchar ett uppslagsfält på ett konto",
|
||||
"salesforce_option": "Salesforce-alternativ",
|
||||
"lookup_field_name": "Uppslagsfältsnamn",
|
||||
"filter_operator_is": "Är",
|
||||
"filter_operator_is_not": "Är inte",
|
||||
"filter_operator_contains": "Innehåller",
|
||||
"filter_operator_does_not_contain": "Innehåller inte",
|
||||
"filter_operator_starts_with": "Börjar med",
|
||||
"filter_operator_ends_with": "Slutar med",
|
||||
"filter_operator_is_empty": "Är tom",
|
||||
"filter_operator_not_empty": "Är inte tom",
|
||||
"translate_description_button": "Översätt beskrivningen till besökarens webbläsarspråk med hjälp av AI",
|
||||
"rr_distribution_method": "Distribution",
|
||||
"rr_distribution_method_description": "Möjliggör optimering av distributionen för maximal tillgänglighet eller för att uppnå en mer balanserad fördelning.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Rezervasyon sınırları başarıyla güncellendi",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Bu rezervasyonda değişiklik yapma yetkiniz yok",
|
||||
"matching_members": "Eşleşen üyeler",
|
||||
"x_matching_members": "{{x}} eşleşen üye",
|
||||
"matching_members_queue_using_attribute_weights": "Üye eşleştirme sırası (özellik ağırlıkları kullanılarak)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Üye eşleştirme sırası (etkinlik atanan kişi ağırlıkları kullanılarak)",
|
||||
"no_matching_members": "Eşleşen üye yok. Etkinlik türüne atanan takım üyeleri kullanılacaktır.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Bu rezervasyonu uygun olmayan bir ev sahibine yeniden atamak istediğinizden emin misiniz?",
|
||||
"reassign_available_team_member_description": "Bu rezervasyonu yeniden atamak istediğinizden emin misiniz?",
|
||||
"yes_reassign": "Evet, yeniden ata",
|
||||
"filter_by_attributes": "Özelliklere göre filtrele",
|
||||
"reassign_reason": "Yeniden atama nedeni",
|
||||
"reassigned_by": "Yeniden atayan",
|
||||
"row": "satır",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Katılımcı Saat Diliminde Etkinlik Bitiş Zamanı",
|
||||
"salesforce_create_event_on_contact": "Eğer varsa, kişi üzerinde etkinlik oluştur. Yoksa potansiyel müşteriye geri dön",
|
||||
"salesforce_owner_name_to_change": "Değiştirilecek sahip adı",
|
||||
"no_filter_set": "Filtre ayarlanmadı",
|
||||
"booking_dry_run_successful": "Rezervasyon test çalıştırması başarılı",
|
||||
"dont_have_access_this_page": "Bu sayfaya erişiminiz yok",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Bu sayfayı görüntülemek veya etkileşimde bulunmak için yönetici veya sahip ayrıcalıklarına sahip olmalısınız",
|
||||
"field_type": "Alan türü",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Bir hesaptaki arama alanıyla eşleşen bir kullanıcıya yönlendir",
|
||||
"salesforce_option": "Salesforce seçeneği",
|
||||
"lookup_field_name": "Arama alanı adı",
|
||||
"filter_operator_is": "Eşittir",
|
||||
"filter_operator_is_not": "Eşit değildir",
|
||||
"filter_operator_contains": "İçerir",
|
||||
"filter_operator_does_not_contain": "İçermez",
|
||||
"filter_operator_starts_with": "İle başlar",
|
||||
"filter_operator_ends_with": "İle biter",
|
||||
"filter_operator_is_empty": "Boş",
|
||||
"filter_operator_not_empty": "Boş değil",
|
||||
"translate_description_button": "Açıklamayı ziyaretçinin tarayıcı diline yapay zeka kullanarak çevir",
|
||||
"rr_distribution_method": "Dağıtım",
|
||||
"rr_distribution_method_description": "Maksimum kullanılabilirlik için dağıtımı optimize etmeye veya daha dengeli bir atama sağlamaya olanak tanır.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Обмеження бронювання успішно оновлено",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Ви не маєте дозволу на внесення цієї зміни до бронювання",
|
||||
"matching_members": "Відповідні учасники",
|
||||
"x_matching_members": "{{x}} відповідних учасників",
|
||||
"matching_members_queue_using_attribute_weights": "Черга відповідних учасників (з використанням ваги атрибутів)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Черга відповідних учасників (з використанням ваги призначених подій)",
|
||||
"no_matching_members": "Відповідних учасників немає. Буде використано учасників команди, призначених для цього типу події.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Ви впевнені, що хочете перепризначити це бронювання недоступному організатору?",
|
||||
"reassign_available_team_member_description": "Ви впевнені, що хочете перепризначити це бронювання?",
|
||||
"yes_reassign": "Так, перепризначити",
|
||||
"filter_by_attributes": "Фільтрувати за атрибутами",
|
||||
"reassign_reason": "Причина перепризначення",
|
||||
"reassigned_by": "Перепризначено",
|
||||
"row": "рядок",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Час закінчення події в часовому поясі учасника",
|
||||
"salesforce_create_event_on_contact": "Створити подію для контакту, якщо він існує. В іншому випадку створити для ліда",
|
||||
"salesforce_owner_name_to_change": "Ім'я власника для зміни",
|
||||
"no_filter_set": "Фільтр не встановлено",
|
||||
"booking_dry_run_successful": "Тестове бронювання успішне",
|
||||
"dont_have_access_this_page": "Ви не маєте доступу до цієї сторінки",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Щоб переглядати або взаємодіяти з цією сторінкою, ви повинні мати права адміністратора або власника",
|
||||
"field_type": "Тип поля",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Маршрутизація до користувача, який відповідає полю пошуку в обліковому записі",
|
||||
"salesforce_option": "Опція Salesforce",
|
||||
"lookup_field_name": "Назва поля пошуку",
|
||||
"filter_operator_is": "Дорівнює",
|
||||
"filter_operator_is_not": "Не дорівнює",
|
||||
"filter_operator_contains": "Містить",
|
||||
"filter_operator_does_not_contain": "Не містить",
|
||||
"filter_operator_starts_with": "Починається з",
|
||||
"filter_operator_ends_with": "Закінчується на",
|
||||
"filter_operator_is_empty": "Порожньо",
|
||||
"filter_operator_not_empty": "Не порожньо",
|
||||
"translate_description_button": "Перекласти опис мовою браузера відвідувача за допомогою ШІ",
|
||||
"rr_distribution_method": "Розподіл",
|
||||
"rr_distribution_method_description": "Дозволяє оптимізувати розподіл для максимальної доступності або для більш збалансованого розподілу завдань.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "Giới hạn đặt lịch đã được cập nhật thành công",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "Bạn không có quyền thực hiện thay đổi này đối với đặt chỗ",
|
||||
"matching_members": "Thành viên phù hợp",
|
||||
"x_matching_members": "{{x}} thành viên phù hợp",
|
||||
"matching_members_queue_using_attribute_weights": "Hàng đợi thành viên phù hợp (sử dụng trọng số thuộc tính)",
|
||||
"matching_members_queue_using_event_assignee_weights": "Hàng đợi thành viên phù hợp (sử dụng trọng số người được chỉ định sự kiện)",
|
||||
"no_matching_members": "Không có thành viên phù hợp. Hệ thống sẽ sử dụng các thành viên trong nhóm được chỉ định cho loại sự kiện.",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "Bạn có chắc chắn muốn phân công lại đặt lịch này cho một chủ trì không khả dụng không?",
|
||||
"reassign_available_team_member_description": "Bạn có chắc chắn muốn phân công lại đặt chỗ này không?",
|
||||
"yes_reassign": "Có, phân công lại",
|
||||
"filter_by_attributes": "Lọc theo thuộc tính",
|
||||
"reassign_reason": "Lý do phân công lại",
|
||||
"reassigned_by": "Được phân công lại bởi",
|
||||
"row": "hàng",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Thời gian kết thúc sự kiện theo múi giờ của Người tham dự",
|
||||
"salesforce_create_event_on_contact": "Tạo sự kiện trên liên hệ, nếu tồn tại. Nếu không, chuyển sang khách hàng tiềm năng",
|
||||
"salesforce_owner_name_to_change": "Tên chủ sở hữu cần thay đổi",
|
||||
"no_filter_set": "Chưa đặt bộ lọc",
|
||||
"booking_dry_run_successful": "Chạy thử đặt lịch thành công",
|
||||
"dont_have_access_this_page": "Bạn không có quyền truy cập trang này",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Để xem hoặc tương tác với trang này, bạn cần có quyền quản trị viên hoặc chủ sở hữu",
|
||||
"field_type": "Loại trường",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "Chuyển đến người dùng phù hợp với trường tra cứu trên tài khoản",
|
||||
"salesforce_option": "Tùy chọn Salesforce",
|
||||
"lookup_field_name": "Tên trường tra cứu",
|
||||
"filter_operator_is": "Là",
|
||||
"filter_operator_is_not": "Không là",
|
||||
"filter_operator_contains": "Chứa",
|
||||
"filter_operator_does_not_contain": "Không chứa",
|
||||
"filter_operator_starts_with": "Bắt đầu bằng",
|
||||
"filter_operator_ends_with": "Kết thúc bằng",
|
||||
"filter_operator_is_empty": "Trống",
|
||||
"filter_operator_not_empty": "Không trống",
|
||||
"translate_description_button": "Dịch mô tả sang ngôn ngữ trình duyệt của khách bằng AI",
|
||||
"rr_distribution_method": "Phân phối",
|
||||
"rr_distribution_method_description": "Cho phép tối ưu hóa việc phân phối để đạt khả năng sẵn sàng tối đa hoặc hướng đến việc phân công cân bằng hơn.",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "预订限制更新成功",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "您无权对此预订进行更改",
|
||||
"matching_members": "匹配的成员",
|
||||
"x_matching_members": "{{x}} 个匹配成员",
|
||||
"matching_members_queue_using_attribute_weights": "使用属性权重的匹配成员队列",
|
||||
"matching_members_queue_using_event_assignee_weights": "使用事件分配者权重的匹配成员队列",
|
||||
"no_matching_members": "没有匹配的成员。将回退到使用分配给事件类型的团队成员。",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "您确定要将此预订重新分配给不可用的主持人吗?",
|
||||
"reassign_available_team_member_description": "您确定要重新分配这个预订吗?",
|
||||
"yes_reassign": "是的,重新分配",
|
||||
"filter_by_attributes": "按属性筛选",
|
||||
"reassign_reason": "重新分配原因",
|
||||
"reassigned_by": "重新分配者",
|
||||
"row": "行",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "参与者时区的事件结束时间",
|
||||
"salesforce_create_event_on_contact": "如果联系人存在,则在联系人上创建事件。否则回退到潜在客户",
|
||||
"salesforce_owner_name_to_change": "要更改的所有者名称",
|
||||
"no_filter_set": "未设置筛选条件",
|
||||
"booking_dry_run_successful": "预订测试成功",
|
||||
"dont_have_access_this_page": "您无权访问此页面",
|
||||
"you_need_admin_or_owner_privileges_to_access": "要查看或操作此页面,您必须拥有管理员或所有者权限",
|
||||
"field_type": "字段类型",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "路由至与账户查找字段匹配的用户",
|
||||
"salesforce_option": "Salesforce选项",
|
||||
"lookup_field_name": "查找字段名称",
|
||||
"filter_operator_is": "等于",
|
||||
"filter_operator_is_not": "不等于",
|
||||
"filter_operator_contains": "包含",
|
||||
"filter_operator_does_not_contain": "不包含",
|
||||
"filter_operator_starts_with": "开头为",
|
||||
"filter_operator_ends_with": "结尾为",
|
||||
"filter_operator_is_empty": "为空",
|
||||
"filter_operator_not_empty": "不为空",
|
||||
"translate_description_button": "使用AI将描述翻译成访问者浏览器的语言",
|
||||
"rr_distribution_method": "分配方式",
|
||||
"rr_distribution_method_description": "可以优化分配方式以实现最大可用性或更均衡的任务分配。",
|
||||
|
||||
@@ -2678,6 +2678,7 @@
|
||||
"booking_limits_updated_successfully": "預約限制更新成功",
|
||||
"you_are_unauthorized_to_make_this_change_to_the_booking": "您無權更改此預訂",
|
||||
"matching_members": "符合條件的成員",
|
||||
"x_matching_members": "{{x}} 個符合的成員",
|
||||
"matching_members_queue_using_attribute_weights": "成員匹配隊列(使用屬性權重)",
|
||||
"matching_members_queue_using_event_assignee_weights": "成員匹配隊列(使用事件指派權重)",
|
||||
"no_matching_members": "沒有符合條件的成員。將回退使用分配給事件類型的團隊成員。",
|
||||
@@ -2747,6 +2748,7 @@
|
||||
"reassign_unavailable_team_member_description": "您確定要將此預訂重新分配給一位不可用的主持人嗎?",
|
||||
"reassign_available_team_member_description": "您確定要重新指派這個預約嗎?",
|
||||
"yes_reassign": "是的,重新分配",
|
||||
"filter_by_attributes": "依屬性篩選",
|
||||
"reassign_reason": "重新分配的原因",
|
||||
"reassigned_by": "重新分配者",
|
||||
"row": "行",
|
||||
@@ -2759,6 +2761,8 @@
|
||||
"event_end_time_in_attendee_timezone_info": "參與者時區的活動結束時間",
|
||||
"salesforce_create_event_on_contact": "如果聯絡人存在,則在聯絡人上建立事件。否則回退到潛在客戶",
|
||||
"salesforce_owner_name_to_change": "要更改的擁有者名稱",
|
||||
"no_filter_set": "未設定篩選條件",
|
||||
"booking_dry_run_successful": "預約測試成功",
|
||||
"dont_have_access_this_page": "您無權存取此頁面",
|
||||
"you_need_admin_or_owner_privileges_to_access": "若要查看或使用此頁面,您必須具有管理員或擁有者權限",
|
||||
"field_type": "欄位類型",
|
||||
@@ -2789,6 +2793,14 @@
|
||||
"salesforce_route_to_custom_lookup_field": "路由至符合帳戶查詢欄位的使用者",
|
||||
"salesforce_option": "Salesforce選項",
|
||||
"lookup_field_name": "查詢欄位名稱",
|
||||
"filter_operator_is": "是",
|
||||
"filter_operator_is_not": "不是",
|
||||
"filter_operator_contains": "包含",
|
||||
"filter_operator_does_not_contain": "不包含",
|
||||
"filter_operator_starts_with": "開頭為",
|
||||
"filter_operator_ends_with": "結尾為",
|
||||
"filter_operator_is_empty": "為空",
|
||||
"filter_operator_not_empty": "不為空",
|
||||
"translate_description_button": "使用 AI 將描述翻譯成訪客瀏覽器的語言",
|
||||
"rr_distribution_method": "分配方式",
|
||||
"rr_distribution_method_description": "可以優化分配方式以達到最大可用性或更平衡的任務分配。",
|
||||
|
||||
@@ -483,3 +483,6 @@ select:focus {
|
||||
.intercom-lightweight-app {
|
||||
@apply z-40 !important;
|
||||
}
|
||||
|
||||
|
||||
@import "./raqb.css";
|
||||
|
||||
@@ -142,6 +142,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
recurringEvent: undefined,
|
||||
eventTypeColor: undefined,
|
||||
userId: 4,
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
},
|
||||
});
|
||||
expect(result.newUserIds).toEqual([4]);
|
||||
@@ -187,6 +189,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
locations: [],
|
||||
scheduleId: null,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
@@ -299,6 +303,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
userId: 4,
|
||||
workflows: undefined,
|
||||
hashedLink: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
},
|
||||
});
|
||||
expect(result.newUserIds).toEqual([4]);
|
||||
@@ -343,6 +349,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...rest,
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
locations: [],
|
||||
hashedLink: {
|
||||
deleteMany: {},
|
||||
@@ -405,6 +413,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
@@ -429,6 +438,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
create: [{ workflowId: 11 }],
|
||||
},
|
||||
hashedLink: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
assignRRMembersUsingSegment: false,
|
||||
},
|
||||
});
|
||||
const { profileId, ...rest } = evType;
|
||||
@@ -437,6 +448,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
data: {
|
||||
...rest,
|
||||
locations: [],
|
||||
assignRRMembersUsingSegment: undefined,
|
||||
rrSegmentQueryValue: undefined,
|
||||
lockTimeZoneToggleOnBookingPage: false,
|
||||
requiresBookerEmailVerification: false,
|
||||
hashedLink: {
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Prisma } from "@prisma/client";
|
||||
import type { WebhookTriggerEvents } from "@prisma/client";
|
||||
import type Stripe from "stripe";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { vi } from "vitest";
|
||||
import "vitest-fetch-mock";
|
||||
import type { z } from "zod";
|
||||
|
||||
@@ -35,6 +36,11 @@ import type { EventBusyDate, IntervalLimit } from "@calcom/types/Calendar";
|
||||
import { getMockPaymentService } from "./MockPaymentService";
|
||||
import type { getMockRequestDataForBooking } from "./getMockRequestDataForBooking";
|
||||
|
||||
// We don't need to test it. Also, it causes Formbricks error when imported
|
||||
vi.mock("@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic", () => ({
|
||||
default: {},
|
||||
}));
|
||||
|
||||
type Fields = z.infer<typeof eventTypeBookingFields>;
|
||||
|
||||
logger.settings.minLevel = 1;
|
||||
|
||||
@@ -2996,6 +2996,7 @@
|
||||
"post": {
|
||||
"operationId": "BookingsController_2024_08_13_cancelBooking",
|
||||
"summary": "Cancel a booking",
|
||||
"description": ":bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.\n For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call\n like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
@@ -11549,6 +11550,13 @@
|
||||
"description": "Location for this booking. Displayed in email and calendar event.",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "You can store any additional data you want here. Metadata must have at most 50 keys, each key up to 40 characters, and string values up to 500 characters.",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -11604,6 +11612,13 @@
|
||||
"description": "Location for this booking. Displayed in email and calendar event.",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "You can store any additional data you want here. Metadata must have at most 50 keys, each key up to 40 characters, and string values up to 500 characters.",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -11664,6 +11679,13 @@
|
||||
"description": "Location for this booking. Displayed in email and calendar event.",
|
||||
"example": "https://example.com/meeting"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"description": "You can store any additional data you want here. Metadata must have at most 50 keys, each key up to 40 characters, and string values up to 500 characters.",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
@@ -11770,6 +11792,12 @@
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -11884,6 +11912,12 @@
|
||||
"type": "boolean",
|
||||
"example": true
|
||||
},
|
||||
"metadata": {
|
||||
"type": "object",
|
||||
"example": {
|
||||
"key": "value"
|
||||
}
|
||||
},
|
||||
"attendees": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
@@ -2680,6 +2680,7 @@ checksums:
|
||||
booking_limits_updated_successfully: c1dbc8e3ecbbc1c25351806a30ab6368
|
||||
you_are_unauthorized_to_make_this_change_to_the_booking: c770486d5b4b98a407ff485cbe60e563
|
||||
matching_members: 8b5c6581fb35a737d20c3ac06b2955b0
|
||||
x_matching_members: a315123fc5073a79e4c2283b46f70954
|
||||
matching_members_queue_using_attribute_weights: cee4482ef0fef095ab86d574667dd505
|
||||
matching_members_queue_using_event_assignee_weights: 87e3f1babccac7a70f5b8d30b2a1827f
|
||||
no_matching_members: 4d8e678a655a70e9ee5c11104d554bfa
|
||||
@@ -2749,6 +2750,7 @@ checksums:
|
||||
reassign_unavailable_team_member_description: 7e07f8972584f6f354e17098517c2ab5
|
||||
reassign_available_team_member_description: 608178100b18b4372e698b61e6c76f7c
|
||||
yes_reassign: 229add6ba9a9bae3a6ee06e3f629b1f5
|
||||
filter_by_attributes: 3d9f996af7690205e6d9b83a4f6a459a
|
||||
reassign_reason: 31d21d7675210253b1405b6b09d3049e
|
||||
reassigned_by: bd6798fd9474a1387504c80a3bbf2b0c
|
||||
row: 49da3a86e7ec17952111c2b50ca30855
|
||||
@@ -2761,6 +2763,8 @@ checksums:
|
||||
event_end_time_in_attendee_timezone_info: 82ffc6118c662094eb130b74a1e59627
|
||||
salesforce_create_event_on_contact: 82fe69b4ba08b1cb9c6465dc18f968b5
|
||||
salesforce_owner_name_to_change: 000522ab557b02cfe4a5d49787259ef9
|
||||
no_filter_set: 5167f195fa1a278506d613d550b578f0
|
||||
booking_dry_run_successful: 9f3d6faa29b1a777f3f2610367da5586
|
||||
dont_have_access_this_page: c2341fe363c89498d3d0deb64f3977b1
|
||||
you_need_admin_or_owner_privileges_to_access: 7f032471f243015def700f16681274b2
|
||||
field_type: 7444da76fa3572d47ea5b5d86c50f11f
|
||||
@@ -2791,6 +2795,14 @@ checksums:
|
||||
salesforce_route_to_custom_lookup_field: 672a24e4de55eb073559e94b9266da7d
|
||||
salesforce_option: f09423b2785ee269db428163fc0a58d0
|
||||
lookup_field_name: 8d81d557128a52661d1ed9e9aae0575c
|
||||
filter_operator_is: 1940eeb4f6f0189788fde5403c6e9e9a
|
||||
filter_operator_is_not: 8c7817ecdb08e6fa92fdf3487e0c8c9d
|
||||
filter_operator_contains: 41c8c25407527a5336404313f4c8d650
|
||||
filter_operator_does_not_contain: d618eb0f854f7efa0d7c644e6628fa42
|
||||
filter_operator_starts_with: f6673c17475708313c6a0f245b561781
|
||||
filter_operator_ends_with: c8a5f60f1bd1d8fa018dbbf49806fb5b
|
||||
filter_operator_is_empty: dca87bc415341b1cdf9523f3b795a313
|
||||
filter_operator_not_empty: 303caa93dcb79e54d47d4cd7d748a1df
|
||||
translate_description_button: 65b94fd5bdb06c983ee3f4c1be74a1ac
|
||||
rr_distribution_method: c3d4633236c01f8314a38a9e9c2361f1
|
||||
rr_distribution_method_description: e9c1dda0299fbfc979a52a79b57e40c7
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@
|
||||
"node-gyp": "^10.2.0",
|
||||
"node-ical": "^0.16.1",
|
||||
"prettier": "^2.8.6",
|
||||
"prismock": "^1.21.1",
|
||||
"prismock": "^1.33.4",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
|
||||
Binary file not shown.
@@ -10,19 +10,21 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
export async function getCRMContactOwnerForRRLeadSkip(
|
||||
bookerEmail: string,
|
||||
eventTypeMetadata: Prisma.JsonValue
|
||||
): Promise<string | undefined> {
|
||||
): Promise<{ email: string | null; recordType: string | null; crmAppSlug: string | null }> {
|
||||
const nullReturnValue = { email: null, recordType: null, crmAppSlug: "" };
|
||||
const parsedEventTypeMetadata = EventTypeMetaDataSchema.safeParse(eventTypeMetadata);
|
||||
if (!parsedEventTypeMetadata.success || !parsedEventTypeMetadata.data?.apps) return;
|
||||
if (!parsedEventTypeMetadata.success || !parsedEventTypeMetadata.data?.apps) return nullReturnValue;
|
||||
|
||||
const crm = await getCRMManagerWithRRLeadSkip(parsedEventTypeMetadata.data.apps);
|
||||
|
||||
if (!crm) return;
|
||||
if (!crm) return nullReturnValue;
|
||||
const { crmManager, crmAppSlug } = crm;
|
||||
const startTime = performance.now();
|
||||
const contact = await crm.getContacts({ emails: bookerEmail, forRoundRobinSkip: true });
|
||||
const contact = await crmManager.getContacts({ emails: bookerEmail, forRoundRobinSkip: true });
|
||||
const endTime = performance.now();
|
||||
logger.info(`Fetching from CRM took ${endTime - startTime}ms`);
|
||||
if (!contact?.length) return;
|
||||
return contact[0].ownerEmail;
|
||||
if (!contact?.length || !contact[0].email) return nullReturnValue;
|
||||
return { email: contact[0].ownerEmail ?? null, recordType: contact[0].recordType ?? null, crmAppSlug };
|
||||
}
|
||||
|
||||
async function getCRMManagerWithRRLeadSkip(apps: z.infer<typeof EventTypeAppMetadataSchema>) {
|
||||
@@ -53,5 +55,8 @@ async function getCRMManagerWithRRLeadSkip(apps: z.infer<typeof EventTypeAppMeta
|
||||
},
|
||||
});
|
||||
if (!crmCredential) return;
|
||||
return new CrmManager(crmCredential, crmRoundRobinLeadSkip);
|
||||
return {
|
||||
crmManager: new CrmManager(crmCredential, crmRoundRobinLeadSkip),
|
||||
crmAppSlug: crmCredential.appId,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
## Ready for Review Requirements
|
||||
- Authorization check for fetch form response and form
|
||||
|
||||
|
||||
|
||||
## TODO - Routing
|
||||
- [x] Use teamMemberIds to calculate availabilithy as well in addition to doing it in handleNewBooking
|
||||
- [x] Don't let the Routing Form with attributesQueryValue be picked
|
||||
- [x] Use a subset of event assignees using teamMemberIds
|
||||
- [x] Ensure that renaming an attribute option doesn't remove it from routing logic
|
||||
- [x] Add support for multiselect_contains and multiselect_not_contains operators in attributes
|
||||
- [x] Fix bug with Multiselect attribute
|
||||
- [x] Connect Form Response to Booking
|
||||
- [x] Test with non option based attributes and form field input
|
||||
- [x] We might want to disable attributes query builder for non-orgs. Same for Re-routing.
|
||||
- [ ] Switching from Select to MultiSelect in attribute
|
||||
- [ ] It immediately makes the Routing logic to not work. Fix it.
|
||||
- [ ] It removes the selected option from Routing. Fix it.
|
||||
@@ -23,10 +9,6 @@
|
||||
- So, if Form Field is 'What is Comapny Size' with options Enterprise, Large, Medium, Small and Attribute for which Value of 'What is compnay size' has options Enterprise, Large, Medium. It would show a warning that 'Small' option is not present in the attribute.
|
||||
- [ ] When logic can't be built(Short text maps to an attribute with given set of options but the short text value doesn't match any of the options), the team member matching route throws error but Test Preview still shows the previous value - It show should the error in there.
|
||||
- [ ] When routing to a team that doesn't have the team members assigned that are chosen, we are unable to select those users. FIX it by ignoring the assigned members(except the fixed hosts) and just using the attributes matched team members.
|
||||
- [x] Start storing eventTypeId in routing Action.
|
||||
- [x] Make sure that without making the FORM dirty, user can't reroute
|
||||
- [x] When 'Verify Route' is clicked, it doesn't seem to run the form field logic as there is a warning in console. Fix it.
|
||||
- [x] Give a proper icon to Reroute button
|
||||
- [ ] Fetch bookings on window focus but not when rerouting dialog is open. As soon as it is closed, we need to refetch again.
|
||||
- [ ] Seated Events
|
||||
- [ ] Disable routing for seated events. Disable only during creation.
|
||||
@@ -45,22 +27,13 @@
|
||||
- [ ] findTeamMembersMatchingAttributesQuery - Parallelize jsonLogic.apply across team members.
|
||||
|
||||
### Unit Tests
|
||||
- [ ] findTeamMembersMatchingAttributesQuery
|
||||
- [x] Basic Tests
|
||||
- [ ] Performance Tests with say 1000 team members and 100 attributes to ensure no regression
|
||||
- [ ] evaluateRAQB
|
||||
- [x] Basic Tests
|
||||
- [ ] More Detailed Tests
|
||||
- [ ] jsonLogic
|
||||
- [x] Basic Tests
|
||||
- [ ] getAttributes
|
||||
- [ ] Querying Logic Test
|
||||
- [x] getRoutedUsers tests
|
||||
- [ ] getAvailableSlots
|
||||
- [ ] should use routedTeamMemberIds in availability query
|
||||
- [ ] RerouteDialog
|
||||
- [x] BASIC
|
||||
- [ ] New Tab rescheduling
|
||||
|
||||
|
||||
### Documentation / Tooltip
|
||||
|
||||
@@ -98,7 +98,7 @@ vi.mock("@calcom/trpc/react", () => ({
|
||||
trpc: {
|
||||
viewer: {
|
||||
routingForms: {
|
||||
findTeamMembersMatchingAttributeLogic: {
|
||||
findTeamMembersMatchingAttributeLogicOfRoute: {
|
||||
useMutation: vi.fn(({ onSuccess }) => {
|
||||
return {
|
||||
mutate: vi.fn(() => {
|
||||
@@ -161,20 +161,44 @@ describe("TestFormDialog", () => {
|
||||
});
|
||||
|
||||
it("renders the dialog when open", () => {
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByText("test_routing_form")).toBeInTheDocument();
|
||||
expect(screen.getByText("test_preview_description")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("doesn't render the dialog when closed", () => {
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={false} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={false}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText("test_routing_form")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders form fields", () => {
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("form-field-name")).toBeInTheDocument();
|
||||
});
|
||||
@@ -182,7 +206,15 @@ describe("TestFormDialog", () => {
|
||||
describe("Team Form", () => {
|
||||
it("submits the form and shows test results for Custom Page", async () => {
|
||||
mockCustomPageMessageMatchingRoute();
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
|
||||
fireEvent.click(screen.getByText("test_routing"));
|
||||
|
||||
@@ -193,7 +225,15 @@ describe("TestFormDialog", () => {
|
||||
|
||||
it("submits the form and shows test results for Event Type", async () => {
|
||||
mockEventTypeRedirectUrlMatchingRoute();
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
|
||||
fireEvent.click(screen.getByText("test_routing"));
|
||||
expect(screen.getByText("route_to:")).toBeInTheDocument();
|
||||
@@ -215,7 +255,15 @@ describe("TestFormDialog", () => {
|
||||
},
|
||||
checkedFallback: false,
|
||||
});
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
|
||||
fireEvent.click(screen.getByText("test_routing"));
|
||||
expect(screen.getByText("route_to:")).toBeInTheDocument();
|
||||
@@ -237,7 +285,15 @@ describe("TestFormDialog", () => {
|
||||
mainWarnings: ["Main-Error-1", "Main-Error-2"],
|
||||
fallbackWarnings: ["Fallback-Error-1", "Fallback-Error-2"],
|
||||
});
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
|
||||
fireEvent.click(screen.getByText("test_routing"));
|
||||
screen.logTestingPlaygroundURL();
|
||||
@@ -255,7 +311,15 @@ describe("TestFormDialog", () => {
|
||||
mainWarnings: null,
|
||||
fallbackWarnings: null,
|
||||
});
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
|
||||
fireEvent.click(screen.getByText("test_routing"));
|
||||
screen.logTestingPlaygroundURL();
|
||||
@@ -273,7 +337,15 @@ describe("TestFormDialog", () => {
|
||||
mainWarnings: null,
|
||||
fallbackWarnings: null,
|
||||
});
|
||||
render(<TestFormDialog form={mockTeamForm} isTestPreviewOpen={true} setIsTestPreviewOpen={() => {}} />);
|
||||
render(
|
||||
<TestFormDialog
|
||||
form={mockTeamForm}
|
||||
isTestPreviewOpen={true}
|
||||
setIsTestPreviewOpen={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
fireEvent.change(screen.getByTestId("form-field-name"), { target: { value: "John Doe" } });
|
||||
fireEvent.click(screen.getByText("test_routing"));
|
||||
expect(screen.getByTestId("attribute-logic-matched")).toHaveTextContent("no");
|
||||
|
||||
@@ -5,7 +5,7 @@ type AppBookingFormHandler = (
|
||||
attendeeEmail: string,
|
||||
attributeRoutingConfig: AttributeRoutingConfig,
|
||||
eventTypeId: number
|
||||
) => Promise<{ email: string | null }>;
|
||||
) => Promise<{ email: string | null; recordType: string | null }>;
|
||||
|
||||
const appBookingFormHandler: Record<string, AppBookingFormHandler> = {
|
||||
salesforce: routingFormBookingFormHandler,
|
||||
|
||||
@@ -9,6 +9,7 @@ import isRouterLinkedField from "../lib/isRouterLinkedField";
|
||||
import { getUIOptionsForSelect } from "../lib/selectOptions";
|
||||
import { getFieldResponseForJsonLogic } from "../lib/transformResponse";
|
||||
import type { SerializableForm, FormResponse } from "../types/types";
|
||||
import { ConfigFor, withRaqbSettingsAndWidgets } from "./react-awesome-query-builder/config/uiConfig";
|
||||
|
||||
export type FormInputFieldsProps = {
|
||||
form: Pick<SerializableForm<App_RoutingForms_Form>, "fields">;
|
||||
@@ -26,7 +27,10 @@ export type FormInputFieldsProps = {
|
||||
export default function FormInputFields(props: FormInputFieldsProps) {
|
||||
const { form, response, setResponse, disabledFields = [] } = props;
|
||||
|
||||
const formFieldsQueryBuilderConfig = getQueryBuilderConfigForFormFields(form);
|
||||
const formFieldsQueryBuilderConfig = withRaqbSettingsAndWidgets({
|
||||
config: getQueryBuilderConfigForFormFields(form),
|
||||
configFor: ConfigFor.FormFields,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -431,7 +431,7 @@ export const TestFormDialog = ({
|
||||
setMembersMatchResult(null);
|
||||
};
|
||||
const findTeamMembersMatchingAttributeLogicMutation =
|
||||
trpc.viewer.routingForms.findTeamMembersMatchingAttributeLogic.useMutation({
|
||||
trpc.viewer.routingForms.findTeamMembersMatchingAttributeLogicOfRoute.useMutation({
|
||||
onSuccess(data) {
|
||||
setMembersMatchResult({
|
||||
isUsingAttributeWeights: data.isUsingAttributeWeights,
|
||||
@@ -708,6 +708,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
|
||||
{t("routing_forms_send_email_to")}
|
||||
</span>
|
||||
<AddMembersWithSwitch
|
||||
teamId={form.teamId}
|
||||
teamMembers={form.teamMembers.map((member) => ({
|
||||
value: member.id.toString(),
|
||||
label: member.name || member.email,
|
||||
|
||||
+13
-6
@@ -7,10 +7,7 @@ import type {
|
||||
Operator as RAQBOperator,
|
||||
} from "react-awesome-query-builder";
|
||||
|
||||
export type Conjunction = Omit<
|
||||
RAQBConjunction,
|
||||
"formatConj" | "sqlFormatConj" | "spelFormatConj" | "mongoConj"
|
||||
>;
|
||||
export type Conjunction = RAQBConjunction;
|
||||
export type Conjunctions = Record<string, Conjunction>;
|
||||
export type Operator = RAQBOperator & {
|
||||
_jsonLogicIsExclamationOp?: boolean;
|
||||
@@ -30,11 +27,19 @@ const conjunctions: Conjunctions = {
|
||||
label: "And",
|
||||
jsonLogicConj: "and",
|
||||
reversedConj: "OR",
|
||||
formatConj: () => "",
|
||||
sqlFormatConj: () => "",
|
||||
spelFormatConj: () => "",
|
||||
mongoConj: "",
|
||||
},
|
||||
OR: {
|
||||
label: "Or",
|
||||
jsonLogicConj: "or",
|
||||
reversedConj: "AND",
|
||||
formatConj: () => "",
|
||||
sqlFormatConj: () => "",
|
||||
spelFormatConj: () => "",
|
||||
mongoConj: "",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -175,7 +180,7 @@ const operators: Operators = {
|
||||
},
|
||||
select_not_any_in: {
|
||||
isNotOp: true,
|
||||
label: "Not in",
|
||||
label: "Not any in",
|
||||
labelForFormat: "NOT IN",
|
||||
reversedOp: "select_any_in",
|
||||
},
|
||||
@@ -438,10 +443,12 @@ const settings: Settings = {
|
||||
setOpOnChangeField: ["keep" as const, "default" as const], // 'default' (default if present), 'keep' (keep prev from last field), 'first', 'none'
|
||||
};
|
||||
|
||||
export default {
|
||||
const basicConfig = {
|
||||
conjunctions,
|
||||
operators,
|
||||
widgets,
|
||||
types,
|
||||
settings,
|
||||
};
|
||||
|
||||
export default basicConfig;
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Figure out why routing-forms/env.d.ts doesn't work
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import type { Operators, Types } from "./BasicConfig";
|
||||
import BasicConfig from "./BasicConfig";
|
||||
import { ConfigFor } from "./types";
|
||||
import type { WidgetsWithoutFactory } from "./types";
|
||||
|
||||
function getWidgetsWithoutFactory(_configFor: ConfigFor) {
|
||||
const widgetsWithoutFactory: WidgetsWithoutFactory = {
|
||||
...BasicConfig.widgets,
|
||||
phone: {
|
||||
...BasicConfig.widgets.text,
|
||||
},
|
||||
email: {
|
||||
...BasicConfig.widgets.text,
|
||||
},
|
||||
};
|
||||
return widgetsWithoutFactory;
|
||||
}
|
||||
|
||||
function getTypes(configFor: ConfigFor) {
|
||||
const multiSelectOperators = BasicConfig.types.multiselect.widgets.multiselect.operators || [];
|
||||
|
||||
if (configFor === ConfigFor.Attributes) {
|
||||
// Attributes don't need reporting at the moment. So, we can support multiselect_some_in and multiselect_not_some_in operators for attributes.
|
||||
// We could probably use them in FormFields later once they are supported through Prisma query as well
|
||||
multiSelectOperators.push("multiselect_some_in", "multiselect_not_some_in");
|
||||
}
|
||||
|
||||
const types: Types = {
|
||||
...BasicConfig.types,
|
||||
phone: {
|
||||
...BasicConfig.types.text,
|
||||
widgets: {
|
||||
...BasicConfig.types.text.widgets,
|
||||
},
|
||||
},
|
||||
email: {
|
||||
...BasicConfig.types.text,
|
||||
widgets: {
|
||||
...BasicConfig.types.text.widgets,
|
||||
},
|
||||
},
|
||||
multiselect: {
|
||||
...BasicConfig.types.multiselect,
|
||||
widgets: {
|
||||
...BasicConfig.types.multiselect.widgets,
|
||||
multiselect: {
|
||||
...BasicConfig.types.multiselect.widgets.multiselect,
|
||||
operators: [...multiSelectOperators],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return types;
|
||||
}
|
||||
|
||||
function getOperators(_configFor: ConfigFor) {
|
||||
// Clone to avoid mutating the original object
|
||||
const operators: Operators = {
|
||||
...BasicConfig.operators,
|
||||
};
|
||||
|
||||
return operators;
|
||||
}
|
||||
|
||||
function getConjunctions(_configFor: ConfigFor) {
|
||||
return {
|
||||
...BasicConfig.conjunctions,
|
||||
};
|
||||
}
|
||||
|
||||
function getSettingsWithoutRenderFns() {
|
||||
return {
|
||||
...BasicConfig.settings,
|
||||
groupActionsPosition: "bottomCenter" as const,
|
||||
// TODO: Test it and then enable it. It might allow us to show better error messages.
|
||||
// But it doesn't detect every kind of error like an operator gone missing e.g. what happened in https://github.com/calcom/cal.com/pull/17102
|
||||
showErrorMessage: true,
|
||||
// Disable groups
|
||||
maxNesting: 1,
|
||||
};
|
||||
}
|
||||
|
||||
export const FormFieldsBaseConfig = {
|
||||
conjunctions: getConjunctions(ConfigFor.FormFields),
|
||||
operators: getOperators(ConfigFor.FormFields),
|
||||
types: getTypes(ConfigFor.FormFields),
|
||||
widgets: getWidgetsWithoutFactory(ConfigFor.FormFields),
|
||||
settings: getSettingsWithoutRenderFns(),
|
||||
};
|
||||
|
||||
export const AttributesBaseConfig = {
|
||||
conjunctions: getConjunctions(ConfigFor.Attributes),
|
||||
operators: getOperators(ConfigFor.Attributes),
|
||||
types: getTypes(ConfigFor.Attributes),
|
||||
widgets: getWidgetsWithoutFactory(ConfigFor.Attributes),
|
||||
settings: getSettingsWithoutRenderFns(),
|
||||
};
|
||||
-198
@@ -1,198 +0,0 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import type {
|
||||
Settings,
|
||||
Widgets,
|
||||
SelectWidgetProps,
|
||||
SelectWidget as SelectWidgetType,
|
||||
} from "react-awesome-query-builder";
|
||||
|
||||
import { EmailField as EmailWidget } from "@calcom/ui";
|
||||
|
||||
import widgetsComponents from "../widgets";
|
||||
// Figure out why routing-forms/env.d.ts doesn't work
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
import type { Operators, Types } from "./BasicConfig";
|
||||
import BasicConfig from "./BasicConfig";
|
||||
|
||||
const enum ConfigFor {
|
||||
FormFields = "FormFields",
|
||||
Attributes = "Attributes",
|
||||
}
|
||||
const {
|
||||
TextWidget,
|
||||
TextAreaWidget,
|
||||
MultiSelectWidget,
|
||||
SelectWidget,
|
||||
NumberWidget,
|
||||
FieldSelect,
|
||||
Conjs,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Provider,
|
||||
} = widgetsComponents;
|
||||
|
||||
const renderComponent = function <T1>(props: T1 | undefined, Component: React.FC<T1>) {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
function getSettings(_configFor: ConfigFor) {
|
||||
const settings: Settings = {
|
||||
...BasicConfig.settings,
|
||||
|
||||
renderField: (props) => renderComponent(props, FieldSelect),
|
||||
renderOperator: (props) => renderComponent(props, FieldSelect),
|
||||
renderFunc: (props) => renderComponent(props, FieldSelect),
|
||||
renderConjs: (props) => renderComponent(props, Conjs),
|
||||
renderButton: (props) => renderComponent(props, Button),
|
||||
renderButtonGroup: (props) => renderComponent(props, ButtonGroup),
|
||||
renderProvider: (props) => renderComponent(props, Provider),
|
||||
|
||||
groupActionsPosition: "bottomCenter",
|
||||
// TODO: Test it and then enable it. It might allow us to show better error messages.
|
||||
// But it doesn't detect every kind of error like an operator gone missing e.g. what happened in https://github.com/calcom/cal.com/pull/17102
|
||||
showErrorMessage: true,
|
||||
// Disable groups
|
||||
maxNesting: 1,
|
||||
};
|
||||
return settings;
|
||||
}
|
||||
|
||||
// react-query-builder types have missing type property on Widget
|
||||
//TODO: Reuse FormBuilder Components - FormBuilder components are built considering Cal.com design system and coding guidelines. But when awesome-query-builder renders these components, it passes its own props which are different from what our Components expect.
|
||||
// So, a mapper should be written here that maps the props provided by awesome-query-builder to the props that our components expect.
|
||||
function getWidgets(_configFor: ConfigFor) {
|
||||
const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string } } = {
|
||||
...BasicConfig.widgets,
|
||||
text: {
|
||||
...BasicConfig.widgets.text,
|
||||
factory: (props) => renderComponent(props, TextWidget),
|
||||
},
|
||||
textarea: {
|
||||
...BasicConfig.widgets.textarea,
|
||||
factory: (props) => renderComponent(props, TextAreaWidget),
|
||||
},
|
||||
number: {
|
||||
...BasicConfig.widgets.number,
|
||||
factory: (props) => renderComponent(props, NumberWidget),
|
||||
},
|
||||
multiselect: {
|
||||
...BasicConfig.widgets.multiselect,
|
||||
factory: (
|
||||
props?: SelectWidgetProps & {
|
||||
listValues: { title: string; value: string }[];
|
||||
}
|
||||
) => renderComponent(props, MultiSelectWidget),
|
||||
} as SelectWidgetType,
|
||||
select: {
|
||||
...BasicConfig.widgets.select,
|
||||
factory: (
|
||||
props: SelectWidgetProps & {
|
||||
listValues: { title: string; value: string }[];
|
||||
}
|
||||
) => renderComponent(props, SelectWidget),
|
||||
} as SelectWidgetType,
|
||||
phone: {
|
||||
...BasicConfig.widgets.text,
|
||||
factory: (props) => {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
return <TextWidget type="tel" {...props} />;
|
||||
},
|
||||
valuePlaceholder: "Enter Phone Number",
|
||||
},
|
||||
email: {
|
||||
...BasicConfig.widgets.text,
|
||||
factory: (props) => {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmailWidget
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
props.setValue(val);
|
||||
}}
|
||||
containerClassName="w-full"
|
||||
className="dark:placeholder:text-darkgray-600 focus:border-brand border-subtle dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
return widgets;
|
||||
}
|
||||
|
||||
function getTypes(configFor: ConfigFor) {
|
||||
const multiSelectOperators = BasicConfig.types.multiselect.widgets.multiselect.operators || [];
|
||||
|
||||
if (configFor === ConfigFor.Attributes) {
|
||||
// Attributes don't need reporting at the moment. So, we can support multiselect_some_in and multiselect_not_some_in operators for attributes.
|
||||
// We could probably use them in FormFields later once they are supported through Prisma query as well
|
||||
multiSelectOperators.push("multiselect_some_in", "multiselect_not_some_in");
|
||||
}
|
||||
|
||||
const types: Types = {
|
||||
...BasicConfig.types,
|
||||
phone: {
|
||||
...BasicConfig.types.text,
|
||||
widgets: {
|
||||
...BasicConfig.types.text.widgets,
|
||||
},
|
||||
},
|
||||
email: {
|
||||
...BasicConfig.types.text,
|
||||
widgets: {
|
||||
...BasicConfig.types.text.widgets,
|
||||
},
|
||||
},
|
||||
multiselect: {
|
||||
...BasicConfig.types.multiselect,
|
||||
widgets: {
|
||||
...BasicConfig.types.multiselect.widgets,
|
||||
multiselect: {
|
||||
...BasicConfig.types.multiselect.widgets.multiselect,
|
||||
operators: [...multiSelectOperators],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return types;
|
||||
}
|
||||
|
||||
function getOperators(configFor: ConfigFor) {
|
||||
// Clone to avoid mutating the original object
|
||||
const operators: Operators = {
|
||||
...BasicConfig.operators,
|
||||
};
|
||||
|
||||
return operators;
|
||||
}
|
||||
|
||||
function getConjunctions(_configFor: ConfigFor) {
|
||||
return {
|
||||
...BasicConfig.conjunctions,
|
||||
};
|
||||
}
|
||||
|
||||
export const FormFieldsBaseConfig = {
|
||||
conjunctions: getConjunctions(ConfigFor.FormFields),
|
||||
operators: getOperators(ConfigFor.FormFields),
|
||||
types: getTypes(ConfigFor.FormFields),
|
||||
widgets: getWidgets(ConfigFor.FormFields),
|
||||
settings: getSettings(ConfigFor.FormFields),
|
||||
};
|
||||
|
||||
export const AttributesBaseConfig = {
|
||||
conjunctions: getConjunctions(ConfigFor.Attributes),
|
||||
operators: getOperators(ConfigFor.Attributes),
|
||||
types: getTypes(ConfigFor.Attributes),
|
||||
widgets: getWidgets(ConfigFor.Attributes),
|
||||
settings: getSettings(ConfigFor.Attributes),
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import type { Widgets as RaqbWidgets } from "react-awesome-query-builder";
|
||||
|
||||
export const enum ConfigFor {
|
||||
FormFields = "FormFields",
|
||||
Attributes = "Attributes",
|
||||
}
|
||||
|
||||
export type Widgets = RaqbWidgets & { [key in keyof RaqbWidgets]: RaqbWidgets[key] & { type: string } };
|
||||
export type WidgetsWithoutFactory = { [key in keyof Widgets]: Omit<Widgets[key], "factory"> };
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import type { ChangeEvent } from "react";
|
||||
import type {
|
||||
Settings,
|
||||
SelectWidgetProps,
|
||||
SelectWidget as SelectWidgetType,
|
||||
WidgetProps,
|
||||
} from "react-awesome-query-builder";
|
||||
|
||||
import { EmailField as EmailWidget } from "@calcom/ui";
|
||||
|
||||
import widgetsComponents from "../widgets";
|
||||
import type { Widgets, WidgetsWithoutFactory } from "./types";
|
||||
import type { ConfigFor } from "./types";
|
||||
|
||||
export { ConfigFor } from "./types";
|
||||
|
||||
const renderComponent = function <T1>(props: T1 | undefined, Component: React.FC<T1>) {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
return <Component {...props} />;
|
||||
};
|
||||
|
||||
const {
|
||||
TextWidget,
|
||||
TextAreaWidget,
|
||||
MultiSelectWidget,
|
||||
SelectWidget,
|
||||
NumberWidget,
|
||||
FieldSelect,
|
||||
Conjs,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Provider,
|
||||
} = widgetsComponents;
|
||||
|
||||
const TextFactory = (props: WidgetProps | undefined) => renderComponent(props, TextWidget);
|
||||
const TextAreaFactory = (props: WidgetProps | undefined) => renderComponent(props, TextAreaWidget);
|
||||
const NumberFactory = (props: WidgetProps | undefined) => renderComponent(props, NumberWidget);
|
||||
const MultiSelectFactory = (
|
||||
props:
|
||||
| (SelectWidgetProps & {
|
||||
listValues: { title: string; value: string }[];
|
||||
})
|
||||
| undefined
|
||||
) => renderComponent(props, MultiSelectWidget);
|
||||
const SelectFactory = (
|
||||
props:
|
||||
| (SelectWidgetProps & {
|
||||
listValues: { title: string; value: string }[];
|
||||
})
|
||||
| undefined
|
||||
) => renderComponent(props, SelectWidget);
|
||||
|
||||
const PhoneFactory = (props: WidgetProps | undefined) => {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
return <TextWidget type="tel" {...props} />;
|
||||
};
|
||||
|
||||
const EmailFactory = (props: WidgetProps | undefined) => {
|
||||
if (!props) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EmailWidget
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
props.setValue(val);
|
||||
}}
|
||||
containerClassName="w-full"
|
||||
className="dark:placeholder:text-darkgray-600 focus:border-brand border-subtle dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// react-query-builder types have missing type property on Widget
|
||||
//TODO: Reuse FormBuilder Components - FormBuilder components are built considering Cal.com design system and coding guidelines. But when awesome-query-builder renders these components, it passes its own props which are different from what our Components expect.
|
||||
// So, a mapper should be written here that maps the props provided by awesome-query-builder to the props that our components expect.
|
||||
function withFactoryWidgets(widgets: WidgetsWithoutFactory) {
|
||||
const widgetsWithFactory: Widgets = {
|
||||
...widgets,
|
||||
text: {
|
||||
...widgets.text,
|
||||
factory: TextFactory,
|
||||
},
|
||||
textarea: {
|
||||
...widgets.textarea,
|
||||
factory: TextAreaFactory,
|
||||
},
|
||||
number: {
|
||||
...widgets.number,
|
||||
factory: NumberFactory,
|
||||
},
|
||||
multiselect: {
|
||||
...widgets.multiselect,
|
||||
factory: MultiSelectFactory,
|
||||
} as SelectWidgetType,
|
||||
select: {
|
||||
...widgets.select,
|
||||
factory: SelectFactory,
|
||||
} as SelectWidgetType,
|
||||
phone: {
|
||||
...widgets.text,
|
||||
factory: PhoneFactory,
|
||||
valuePlaceholder: "Enter Phone Number",
|
||||
},
|
||||
email: {
|
||||
...widgets.text,
|
||||
factory: EmailFactory,
|
||||
},
|
||||
};
|
||||
return widgetsWithFactory;
|
||||
}
|
||||
|
||||
function withRenderFnsSettings(settings: Settings) {
|
||||
const settingsWithRenderFns: Settings = {
|
||||
...settings,
|
||||
renderField: (props) => renderComponent(props, FieldSelect),
|
||||
renderOperator: (props) => renderComponent(props, FieldSelect),
|
||||
renderFunc: (props) => renderComponent(props, FieldSelect),
|
||||
renderConjs: (props) => renderComponent(props, Conjs),
|
||||
renderButton: (props) => renderComponent(props, Button),
|
||||
renderButtonGroup: (props) => renderComponent(props, ButtonGroup),
|
||||
renderProvider: (props) => renderComponent(props, Provider),
|
||||
};
|
||||
return settingsWithRenderFns;
|
||||
}
|
||||
|
||||
export function withRaqbSettingsAndWidgets<T extends { widgets: WidgetsWithoutFactory; settings: Settings }>({
|
||||
config,
|
||||
}: {
|
||||
config: T;
|
||||
configFor: ConfigFor;
|
||||
}) {
|
||||
const { settings, widgets, ...rest } = config;
|
||||
return {
|
||||
...rest,
|
||||
settings: withRenderFnsSettings(settings),
|
||||
widgets: withFactoryWidgets(widgets),
|
||||
};
|
||||
}
|
||||
@@ -111,10 +111,18 @@ function transformAttributesToCompatibleFormat(attributes: Attribute[]) {
|
||||
|
||||
export function getQueryBuilderConfigForAttributes({
|
||||
attributes,
|
||||
form,
|
||||
/**
|
||||
* It is the fields that makes up additional options to be matched with for the single select/multiselect attributes.
|
||||
* They are shown as 'Value of field <field-label>' in the dropdown.
|
||||
*/
|
||||
dynamicOperandFields = [],
|
||||
}: {
|
||||
attributes: Attribute[];
|
||||
form: Pick<RoutingForm, "fields">;
|
||||
|
||||
dynamicOperandFields?: {
|
||||
label: string;
|
||||
id: string;
|
||||
}[];
|
||||
}) {
|
||||
const transformedAttributes = transformAttributesToCompatibleFormat(attributes);
|
||||
const fields: RaqbConfigFields = {};
|
||||
@@ -124,16 +132,15 @@ export function getQueryBuilderConfigForAttributes({
|
||||
// We can assert the type because otherwise we throw 'Unsupported field type' error
|
||||
const widget = FormFieldsInitialConfig.widgets[attributeType];
|
||||
const widgetType = widget.type;
|
||||
const attributeOptions = attribute.options.concat(
|
||||
(() => {
|
||||
const formFields = form.fields || [];
|
||||
const formFieldsOptions = formFields.map((field) => ({
|
||||
title: `Value of field '${field.label}'`,
|
||||
value: `{field:${field.id}}`,
|
||||
}));
|
||||
return formFieldsOptions;
|
||||
})()
|
||||
);
|
||||
const valueOfFieldOptions = (() => {
|
||||
const formFieldsOptions = dynamicOperandFields.map((field) => ({
|
||||
title: `Value of field '${field.label}'`,
|
||||
value: `{field:${field.id}}`,
|
||||
}));
|
||||
return formFieldsOptions;
|
||||
})();
|
||||
|
||||
const attributeOptions = [...valueOfFieldOptions, ...attribute.options];
|
||||
|
||||
// These are RAQB fields
|
||||
fields[attribute.id] = {
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
|
||||
const prisma = (await import("@calcom/prisma")).default;
|
||||
const routesParsed = zodRoutes.safeParse(form.routes);
|
||||
if (!routesParsed.success) {
|
||||
log.error("Error parsing routes", safeStringify(routesParsed.error));
|
||||
log.error("Error parsing routes", safeStringify({ error: routesParsed.error, routes: form.routes }));
|
||||
throw new Error("Error parsing routes");
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
|
||||
const fields = parsedFields as NonNullable<z.infer<typeof zodFieldsView>>;
|
||||
|
||||
const fieldsExistInForm: Record<string, true> = {};
|
||||
parsedFields?.forEach((f) => {
|
||||
parsedFields.forEach((f) => {
|
||||
fieldsExistInForm[f.id] = true;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import type { App_RoutingForms_Form } from "@prisma/client";
|
||||
import type { JsonTree } from "react-awesome-query-builder";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { evaluateRaqbLogic, RaqbLogicResult } from "@calcom/lib/raqb/evaluateRaqbLogic";
|
||||
|
||||
import type { FormResponse, Route, SerializableForm } from "../types/types";
|
||||
import type { zodNonRouterRoute } from "../zod";
|
||||
import { evaluateRaqbLogic, RaqbLogicResult } from "./evaluateRaqbLogic";
|
||||
import { getQueryBuilderConfigForFormFields } from "./getQueryBuilderConfig";
|
||||
import { isFallbackRoute } from "./isFallbackRoute";
|
||||
import isRouter from "./isRouter";
|
||||
@@ -50,7 +52,7 @@ export function findMatchingRoute({
|
||||
);
|
||||
|
||||
const result = evaluateRaqbLogic({
|
||||
queryValue: route.queryValue,
|
||||
queryValue: route.queryValue as JsonTree,
|
||||
queryBuilderConfig,
|
||||
data: responseValues,
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import { Button, showToast } from "@calcom/ui";
|
||||
import SingleForm, {
|
||||
getServerSidePropsForSingleFormView as getServerSideProps,
|
||||
} from "../../components/SingleForm";
|
||||
import "../../components/react-awesome-query-builder/styles.css";
|
||||
import type { JsonLogicQuery } from "../../jsonLogicToPrisma";
|
||||
import {
|
||||
getQueryBuilderConfigForFormFields,
|
||||
|
||||
@@ -5,13 +5,15 @@ import Link from "next/link";
|
||||
import React, { useCallback, useState, useEffect } from "react";
|
||||
import { Query, Builder, Utils as QbUtils } from "react-awesome-query-builder";
|
||||
import type { ImmutableTree, BuilderProps, Config } from "react-awesome-query-builder";
|
||||
import type { JsonTree } from "react-awesome-query-builder";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { areTheySiblingEntitites } from "@calcom/lib/entityPermissionUtils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { buildEmptyQueryValue } from "@calcom/lib/raqb/raqbUtils";
|
||||
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/client";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -31,7 +33,10 @@ import type { RoutingFormWithResponseCount } from "../../components/SingleForm";
|
||||
import SingleForm, {
|
||||
getServerSidePropsForSingleFormView as getServerSideProps,
|
||||
} from "../../components/SingleForm";
|
||||
import "../../components/react-awesome-query-builder/styles.css";
|
||||
import {
|
||||
withRaqbSettingsAndWidgets,
|
||||
ConfigFor,
|
||||
} from "../../components/react-awesome-query-builder/config/uiConfig";
|
||||
import { RoutingPages } from "../../lib/RoutingPages";
|
||||
import { createFallbackRoute } from "../../lib/createFallbackRoute";
|
||||
import getEventTypeAppMetadata from "../../lib/getEventTypeAppMetadata";
|
||||
@@ -104,7 +109,7 @@ const hasRules = (route: EditFormRoute) => {
|
||||
};
|
||||
|
||||
function getEmptyQueryValue() {
|
||||
return { id: QbUtils.uuid(), type: "group" };
|
||||
return buildEmptyQueryValue();
|
||||
}
|
||||
|
||||
const getEmptyRoute = (): Exclude<SerializableRoute, GlobalRoute> => {
|
||||
@@ -341,20 +346,16 @@ const Route = ({
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const chosenEventTypeForRedirect = eventTypeRedirectUrlSelectedOption?.eventTypeId
|
||||
? eventTypesMap.get(eventTypeRedirectUrlSelectedOption.eventTypeId)
|
||||
: null;
|
||||
|
||||
const isRoundRobinEventSelectedForRedirect =
|
||||
chosenEventTypeForRedirect?.schedulingType === SchedulingType.ROUND_ROBIN;
|
||||
|
||||
const formFieldsQueryBuilder = shouldShowFormFieldsQueryBuilder ? (
|
||||
<div>
|
||||
<span className="text-emphasis flex w-full items-center text-sm">
|
||||
For responses matching the following criteria (matches all by default)
|
||||
</span>
|
||||
<Query
|
||||
{...(formFieldsQueryBuilderConfig as unknown as Config)}
|
||||
{...withRaqbSettingsAndWidgets({
|
||||
config: formFieldsQueryBuilderConfig,
|
||||
configFor: ConfigFor.FormFields,
|
||||
})}
|
||||
value={route.formFieldsQueryBuilderState.tree}
|
||||
onChange={(immutableTree, formFieldsQueryBuilderConfig) => {
|
||||
onChangeFormFieldsQuery(
|
||||
@@ -370,6 +371,13 @@ const Route = ({
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const attributesQueryBuilderConfigWithRaqbSettingsAndWidgets = attributesQueryBuilderConfig
|
||||
? withRaqbSettingsAndWidgets({
|
||||
config: attributesQueryBuilderConfig,
|
||||
configFor: ConfigFor.Attributes,
|
||||
})
|
||||
: null;
|
||||
|
||||
const attributesQueryBuilder =
|
||||
route.action?.type === RouteActionType.EventTypeRedirectUrl && isTeamForm ? (
|
||||
<div className="mt-4">
|
||||
@@ -391,9 +399,9 @@ const Route = ({
|
||||
) : null}
|
||||
|
||||
<div className="mt-2">
|
||||
{route.attributesQueryBuilderState && attributesQueryBuilderConfig && (
|
||||
{route.attributesQueryBuilderState && attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
|
||||
<Query
|
||||
{...(attributesQueryBuilderConfig as unknown as Config)}
|
||||
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
|
||||
value={route.attributesQueryBuilderState.tree}
|
||||
onChange={(immutableTree, attributesQueryBuilderConfig) => {
|
||||
onChangeTeamMembersQuery(
|
||||
@@ -416,20 +424,21 @@ const Route = ({
|
||||
{t("fallback_attribute_logic_description")}
|
||||
</span>
|
||||
<div className="mt-2">
|
||||
{route.fallbackAttributesQueryBuilderState && attributesQueryBuilderConfig && (
|
||||
<Query
|
||||
{...(attributesQueryBuilderConfig as unknown as Config)}
|
||||
value={route.fallbackAttributesQueryBuilderState.tree}
|
||||
onChange={(immutableTree, attributesQueryBuilderConfig) => {
|
||||
onChangeFallbackTeamMembersQuery(
|
||||
route,
|
||||
immutableTree,
|
||||
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
|
||||
);
|
||||
}}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
)}
|
||||
{route.fallbackAttributesQueryBuilderState &&
|
||||
attributesQueryBuilderConfigWithRaqbSettingsAndWidgets && (
|
||||
<Query
|
||||
{...attributesQueryBuilderConfigWithRaqbSettingsAndWidgets}
|
||||
value={route.fallbackAttributesQueryBuilderState.tree}
|
||||
onChange={(immutableTree, attributesQueryBuilderConfig) => {
|
||||
onChangeFallbackTeamMembersQuery(
|
||||
route,
|
||||
immutableTree,
|
||||
attributesQueryBuilderConfig as unknown as AttributesQueryBuilderConfigWithRaqbFields
|
||||
);
|
||||
}}
|
||||
renderBuilder={renderBuilder}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
@@ -600,7 +609,7 @@ const buildState = <
|
||||
queryValue,
|
||||
config,
|
||||
}: T) => ({
|
||||
tree: QbUtils.checkTree(QbUtils.loadTree(queryValue), config as unknown as Config),
|
||||
tree: QbUtils.checkTree(QbUtils.loadTree(queryValue as JsonTree), config as unknown as Config),
|
||||
config,
|
||||
});
|
||||
|
||||
@@ -736,7 +745,7 @@ const Routes = ({
|
||||
const attributesQueryBuilderConfig = attributes
|
||||
? getQueryBuilderConfigForAttributes({
|
||||
attributes: attributes,
|
||||
form: hookForm.getValues(),
|
||||
dynamicOperandFields: hookForm.getValues().fields,
|
||||
})
|
||||
: null;
|
||||
|
||||
|
||||
@@ -61,11 +61,10 @@ async function addAttributeRoutingRule(page: Page) {
|
||||
});
|
||||
|
||||
const attributeValueSelector = attributeQueryBuilder.locator(".rule--value").nth(0);
|
||||
const numOfOptionsInAttribute = 3;
|
||||
await selectFirstValueForAttributeValue({
|
||||
fromLocator: attributeValueSelector,
|
||||
// Select 'Value of Field Short Text' option
|
||||
option: numOfOptionsInAttribute + 1,
|
||||
option: 1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -326,14 +325,15 @@ test.describe("Routing Forms", () => {
|
||||
// TODO: Find a way to incorporate Routed To and Booked At into the report
|
||||
// @see https://github.com/calcom/cal.com/pull/17229
|
||||
"Routed To",
|
||||
"Assignment Reason",
|
||||
"Booked At",
|
||||
"Submitted At",
|
||||
]);
|
||||
/* Last two columns are "Routed To" and "Booked At" */
|
||||
expect(responses).toEqual([
|
||||
["custom-page", "Option-2", "Option-2", "Option-2", "Option-2", "", "", expect.any(String)],
|
||||
["external-redirect", "Option-2", "Option-2", "Option-2", "Option-2", "", "", expect.any(String)],
|
||||
["event-routing", "Option-2", "Option-2", "Option-2", "Option-2", "", "", expect.any(String)],
|
||||
["custom-page", "Option-2", "Option-2", "Option-2", "Option-2", "", "", "", expect.any(String)],
|
||||
["external-redirect", "Option-2", "Option-2", "Option-2", "Option-2", "", "", "", expect.any(String)],
|
||||
["event-routing", "Option-2", "Option-2", "Option-2", "Option-2", "", "", "", expect.any(String)],
|
||||
]);
|
||||
|
||||
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user