Compare commits

..
Author SHA1 Message Date
Alex van Andel da67be475f chore: release v5.8.2
Post release / release (push) Canceled after 0s
2025-10-22 14:20:43 +01:00
e639237088 feat: SMS workflow action for form triggers #3 (#23673)
* allow routing forms for activeOn

* use repository function to get routing forms

* remove unnecessary code

* adjust logic in update handler

* add triggers to api v2

* remvoe unused file

* rename to getAcitveOnOptions handler

* remove routingFormOptions handler

* clean up getActiveOnOptions

* refactor WorkflowService

* remove logs

* remove unused

* fix: type check

* fix: missed before after events for recurring

* fix: calendarEvent handleMarkNoShow

* fix error message

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* don't query disabled routing forms

* create tasker function

* add tasker code

* move isFormTrigger function

* small adjustments + todo comments

* remove email to host action for form triggers

* throw trpc error if email to host is added as step

* fix dialog on how to use form responses as variables

* remove add variable dropdown for form triggers

* remove form workfows in event workflows tab

* improvements for workflow logic on form submission

* review fixes

* base setup for seperate schedule functions (evt and form)

* add missing BOOKING_PAID workflow trigger

* fix pathname

* fix: test for BOOKING_REQUESTED

* fix activeOn ids

* pass hideBranding and smsReminderNumber

* adjustments to reminderScheduler

* create empty scheduelForForm functions

* pass locale and timezone with form user

* pass formData instead of responses

* pass timeFormat and locale

* reusable function for email sending and reminder creation

* implement scheduleEmailReminderForForm

* remove added editor field from merge conflict

* don't support cal.ai action with form triggers

* throw bad request if form trigger and cal.ai is combined

* add tests for scheduleFormWorkflows

* add form submission tests

* remove form response varibe info

* clean up workflow actions

* fixes for getting template options

* pass triggerType to getAllWorkflows

* move reusable logic to scheduleSMSReminder

* add formdata to param type

* type fixes for text reminder managers

* implement scheduleSMSReminderForForm

* fix import

* fix isAuthorizedToAddActiveOnIds

* disble whatsapp action

* implement triggerFormSubmittedNoEventWorkflow

* code clean up

* Merge branch 'devin/1755107037-add-workflow-triggers' into feat/routing-form-workflow-triggers

* fix type errors

* remove async from getSubmitterEmail

* fix type errors

* revert cal.ai changes

* fix type error

* add sublogger

* code clean up

* fix type errors

* remove label for attendee whatsapp action

* code clean up

* fixes saving teams on org workflows

* fix type error

* code improvements for activeOn ids

* Revert "code improvements for activeOn ids"

This reverts commit 0a3590a4e2ce541b17d63483ad86ed458795a6a3.

* improve variable name

* fix unit tests

* small fixes

* type fixes

* remove unused translation keys

* fix merge conflict issues

* code clean up

* remove SMS action support

* remove more SMS code

* add missing imports

* set custom template for form action

* type fixes

* fix tasker endpoint

* fix duplicate check

* fix workfows.test.ts

* use repository funciton to getHideBranding

* add  back SMS action

* add back changes to smsReminderManager

* code clean up

* fix hasDuplicateSubmission

* code clean up

* select only needed properties

* remove repository functions

* Revert "remove repository functions"

This reverts commit 7aa47b1c59c9abd7f964ebf26f746934c53a44f4.

* add scheduleWorkflows function

* Revert "add scheduleWorkflows function"

This reverts commit fe5db4fe3b65e2743c95475d585300cab98beed7.

* move type to /types

* Revert "move type to /types"

This reverts commit 91e0152154594b3772a801a426260068a8ccea54.

* revert changes causing type errors

* remove import

* remove unused import

* Revert "remove unused import"

This reverts commit 1916768c875ea5d0ac5598ccb8f9c796c5622dc9.

* revert changed from attempt to fix type errors

* pass object to gt all workflows

* fix isAuthorized check

* trigger filtering

* remove form submitted no event booked code

* remove form submitted no event from schema

* remove more code

* remove test

* fixes

* add getSubmitterEmail function

* add trigger

* small fixes

* add missing workflow DTOs

* small fixes

* use activeOnWithChildren

* fix active on when switching trigger type

* remove add variable dropdown

* add getAllWorkflowsFromRoutingForm to WorkflowService

* fix error caused by undefined evt

* fix type error

* fix type error

* fix tests

* code clean up

* final fixes and clean up

* remove console.log

* remove template text form from triggers

* add routing form repoditory function

* fix bug with key

* add comments

* fix test

* add missing await

* use predefined FormSubmissionData type

* add .trim() to sms message

* pass contextData instead

* add missing trigger in update-workflow.input.ts

* ForEvt and ForForm function for aiPhoneCallManager

* chore: add support for form workflows on api v2

* fixup! chore: add support for form workflows on api v2

* use only repository functions in update handler

* move all prisma queries from list.handler

* review suggestions

* chore: handle workflows api v2

* chore: handle workflows api v2, split in 2 endpoints

* fix workflow step creation

* remove connect agent and fixes types

* add type to workflow

* chore: use workflow type in apiv2 WorkflowsOutputService

* update worklfow type on update

* chore: use workflow type in apiv2 WorkflowsOutputService

* fix template body for torm trigger

* some UI fixes for email subject/body

* resetting email body when changing form triggers

* use type field to query workflows

* clean up all old active on values

* remove responseId from all funciton calls

* remove undefined from updateTemplate

* refactor: split routing form and event-type workflows code

* refactor: split routing form and event-type workflows code

* fix template  text when adding action

* chore: don't rename WorkflowActivationDto to avoid ci blocking

* refine update schedule to use only allowed actions

* fix type error

* don't allow whatsapp action with form trigger

* fix type error

* return early if activeOn array is empty

* fix: from step type in BaseFormWorkflowStepDto

* fixup! fix: from step type in BaseFormWorkflowStepDto

* api v2 updates

* move all prisma calls to repository (service/workflows.ts)

* use FORM_TRIGGER_WORKFLOW_EVENTS for form queries

* use userRepository

* use FORM_TRIGGER_WORKFLOW_EVENTS in isFormTrigger

* code clean up

* code clean up

* use repository functions in formSubmissionValidation.ts

* remove action check in update handler

* add back trpc  import

* fix agent repository functions

* add SMS actions to allowed form action constants

* remove unsued import

* fixes for offset api v2

* add missing responseId

* fix failing test

* fix failing test

* remove unused imports

* chore: handle sms step action for form worklfow in dtos

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-10-21 15:28:10 +00:00
Dhairyashil Shinde 5bc1fbb8c2 fix: Allow booking details view for round robin with no hosts (#24250)
* Fix: Allow booking details view for round robin with no hosts

* Fix: Allow booking details view for round robin with no hosts

* fix: include the missing fields with defaults

* fix: include the missing fields with defaults
2025-10-21 14:04:42 +00:00
Volnei MunhozandDevin AI e310931417 fix: remove circular dependency in packages/prisma (#24596)
- Replaced DeploymentRepository import with inline implementation
- Created InlineDeploymentRepository class implementing IDeploymentRepository
- Breaks circular dependency: deployment.ts -> prisma/index.ts -> usage-tracking.ts -> deployment.ts
- Verified with madge: no circular dependencies found

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 13:14:15 +00:00
Alex van Andel 17944311c3 chore: Remove inMaintenanceMode from APIv1 for increased robustness (#24576) 2025-10-21 12:58:31 +00:00
Volnei MunhozandDevin AI d82f87aa2a perf: Remove circular dependencies - wip (#24586)
* wip

* refactor(emails): remove circular dependencies from packages/emails

- Extract renderEmail imports to use direct path (../src/renderEmail) instead of index
- Create utility files for shared types and functions:
  - lib/utils/team-invite-utils.ts: TeamInvite type and utility functions
  - lib/utils/booking-redirect-types.ts: IBookingRedirect type
  - lib/utils/email-types.ts: OrganizationCreation and EmailVerifyCode types
  - lib/utils/date-formatting.ts: getFormattedDate utility function
- Update all template files to import renderEmail directly from ../src/renderEmail
- Update React template components to import from utility files instead of class templates
- This eliminates all circular dependencies within packages/emails (reduced from 234 to 0 internal circular deps)

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* refactor(trpc): remove circular dependency in routing-forms

- Extract ZFormByResponseIdInputSchema to separate schema file
- Create getResponseWithFormFields.schema.ts to break circular dependency
- Update imports in _router.ts and getResponseWithFormFields.handler.ts
- Fix eslint warnings by updating deprecated rule names
- This eliminates the circular dependency within packages/trpc (reduced from 1 to 0 internal circular deps)

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* fix(emails): correct TeamInvite type definition

- Make isExistingUserMovedToOrg required (not optional)
- Make prevLink and newLink non-optional (string | null)
- Add back JSDoc comment for isAutoJoin field
- This fixes type check errors in CI

Co-Authored-By: Volnei Munhoz <volnei.munhoz@gmail.com>

* wip

* Fixes types folder

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 12:24:47 +00:00
Volnei MunhozandDevin AI 6aa21aceb8 refactor: remove unused packages from transpiledPackages (#24595)
Remove @calcom/embed-react and @calcom/embed-snippet from the
transpiledPackages configuration in apps/web/next.config.js as they
are not imported from source in apps/web.

Only @calcom/embed-core is actually used and requires transpilation
since it's imported from source paths like @calcom/embed-core/src/embed-iframe.

The other two packages have pre-built dist files and are not directly
imported in the web app, so Next.js doesn't need to transpile them.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-21 12:07:33 +00:00
Hariom Balhara 870ad57a7e Rename handleNewBooking to BookingCreateService (#23682) 2025-10-21 14:52:16 +03:00
fddee71829 refactor: extract bookings list and calendar views (#24486)
* refactor: extract bookings list and calendar views with nuqs state management

- Extract list-related code into BookingsListView component
- Create empty BookingsCalendarView component for future implementation
- Add nuqs query param state management for view toggle (defaults to list)
- Update bookings-listing-view to conditionally render views
- No visible changes to users (list view remains default)

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: move data fetching logic to parent component

- Keep useFilterValue calls, trpc query, columns, flatData, bookingsToday, finalData, and table setup in parent component
- BookingsListView now receives data as props instead of fetching it
- This allows both list and calendar views to share the same data source

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* fix: add customView column back to render booking items

The customView column was inadvertently removed during refactoring. This column is crucial as it renders the actual BookingListItem components, the "today" header, and the "next" header for the bookings list.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* refactor: rename files for better clarity

- Renamed bookings-listing-view.tsx → bookings-view.tsx (parent view)
- Renamed bookings-list-view.tsx → BookingsList.tsx (list component)
- Renamed bookings-calendar-view.tsx → BookingsCalendar.tsx (calendar component)
- Moved list and calendar components from views/ to components/ directory
- Updated all imports to reflect new structure

This creates a clearer hierarchy where -view is the orchestrator and components are the renderers.

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* clean up implementation

* clean up types

* revert unnecessary changes

* Update packages/features/data-table/GUIDE.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-10-21 11:09:47 +00:00
David Johnson 6f15a5ab0f Add turbo package to dependencies (#24572)
This is needed due to Docker build errors indicating that turbo was not an available command.
2025-10-21 09:05:55 +00:00
Dhairyashil Shinde 1fd64c5d7b chore: improve text for booker active booking limit (#24526)
* chore: improve text for booker active booking limit

* update keys to new one for lingo .dev future recognition
2025-10-21 08:54:16 +00:00
Parship Chowdhury f8614f11ff fix: Description field duplicating content when editing Event Types (#24233)
* fix: description field duplicating content when editing Event Types

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>

* optimal approach

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>

* rm dead code

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>

---------

Signed-off-by: Parship Chowdhury <i.am.parship@gmail.com>
2025-10-21 04:35:37 -03:00
Hariom Balhara 5e5e77236a chore: Add skeleton for BookingCancelService (#24038) 2025-10-21 12:29:17 +05:30
Daniel Adeboye 4cee53b07c feat: support northflank deployment (#24571) 2025-10-20 19:47:06 +00:00
Eunjae LeeandDevin AI c289fac008 refactor: make timezone a required prop in Calendar component (#24567)
* refactor: make timezone a required prop in Calendar component

- Remove useBookerTime hook from Calendar.tsx
- Add timezone as required prop in CalendarState type
- Update all parent components to pass timezone prop:
  - packages/features/bookings/Booker/components/LargeCalendar.tsx
  - packages/features/calendar-view/LargeCalendar.tsx
  - packages/features/troubleshooter/components/LargeCalendar.tsx
- All parent components now use useTimePreferences hook to get timezone

Co-Authored-By: eunjae@cal.com <hey@eunjae.dev>

* use right implementation

* remove a line

* use user's timezone instead of booker timezone

* get timezone from calendar store

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-20 13:59:59 +00:00
Keith WilliamsandDevin AI 68f9e86cdd fix: remove circular dependency by moving useLastUsed hook to auth features (#24552)
- Move useLastUsed hook from packages/lib/hooks to packages/features/auth/lib/hooks
- Update imports in SAMLLogin.tsx and login-view.tsx
- Remove cross-package tsconfig include of ../trpc/types/router.d.ts from packages/lib/tsconfig.json

This fixes the issue where the IDE was unnecessarily initializing packages/ui/tsconfig.json
when navigating to definitions in packages/lib. The hook was importing from @calcom/ui/classNames,
creating a circular dependency since packages/ui depends on packages/lib.

By moving the hook to packages/features/auth where it's actually used, we eliminate the
circular dependency without adding tailwind dependencies to packages/lib.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-10-19 10:14:45 -03:00
Keith Williams 5da754db65 perf: Remove tRPC types from @calcom/lib (#24551) 2025-10-19 13:14:25 +00:00
135 changed files with 1154 additions and 666 deletions
+1 -1
View File
@@ -111,7 +111,7 @@ The event types page UI components are located in `apps/web/modules/event-types/
Changes to shared UI patterns (like tab layouts and button alignments) need to be checked across multiple views to maintain consistency:
- Event types page layout: `apps/web/modules/event-types/views/event-types-listing-view.tsx`
- Bookings page layout: `apps/web/modules/bookings/views/bookings-listing-view.tsx`
- Bookings page layout: `apps/web/modules/bookings/views/bookings-view.tsx`
- Common elements like tabs, search bars, and filter buttons should maintain consistent alignment across views
## When working on workflow triggers or similar enum-based features in the Cal.com codebase
View File
+8 -1
View File
@@ -234,9 +234,10 @@ for Logger level to be set at info, for example.
</details>
If you don't want to create a local DB. Then you can also consider using services like railway.app or render.
If you don't want to create a local DB. Then you can also consider using services like railway.app, Northflank or render.
- [Setup postgres DB with railway.app](https://docs.railway.app/guides/postgresql)
- [Setup postgres DB with Northflank](https://northflank.com/guides/deploy-postgres-database-on-northflank)
- [Setup postgres DB with render](https://render.com/docs/databases)
1. Copy and paste your `DATABASE_URL` from `.env` to `.env.appStore`.
@@ -390,6 +391,12 @@ Cal.com, Inc. does not provide official support for Docker, but we will accept f
You can deploy Cal.com on [Railway](https://railway.app) using the button above. The team at Railway also have a [detailed blog post](https://blog.railway.app/p/calendso) on deploying Cal.com on their platform.
### Northflank
[![Deploy on Northflank](https://assets.northflank.com/deploy_to_northflank_smm_36700fb050.svg)](https://northflank.com/stacks/deploy-calcom)
You can deploy Cal.com on [Northflank](https://northflank.com) using the button above. The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying Cal.com on their platform.
### Vercel
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
@@ -1,23 +0,0 @@
import { get } from "@vercel/edge-config";
import type { NextMiddleware } from "next-api-middleware";
const safeGet = async <T = unknown>(key: string): Promise<T | undefined> => {
try {
return get<T>(key);
} catch (error) {
// Don't crash if EDGE_CONFIG env var is missing
}
};
export const config = { matcher: "/:path*" };
export const checkIsInMaintenanceMode: NextMiddleware = async (req, res, next) => {
const isInMaintenanceMode = await safeGet<boolean>("isInMaintenanceMode");
if (isInMaintenanceMode) {
return res
.status(503)
.json({ message: "API is currently under maintenance. Please try again at a later time." });
}
await next();
};
+2 -6
View File
@@ -3,7 +3,6 @@ import { label } from "next-api-middleware";
import { addRequestId } from "./addRequestid";
import { captureErrors } from "./captureErrors";
import { captureUserId } from "./captureUserId";
import { checkIsInMaintenanceMode } from "./checkIsInMaintenanceMode";
import { extendRequest } from "./extendRequest";
import {
HTTP_DELETE,
@@ -26,7 +25,6 @@ const middleware = {
HTTP_POST,
HTTP_DELETE,
addRequestId,
checkIsInMaintenanceMode,
verifyApiKey,
rateLimitApiKey,
extendRequest,
@@ -38,16 +36,14 @@ const middleware = {
type Middleware = keyof typeof middleware;
const middlewareOrder = [
// The order here, determines the order of execution
"checkIsInMaintenanceMode",
const middlewareOrder: Middleware[] = [
"extendRequest",
"captureErrors",
"verifyApiKey",
"rateLimitApiKey",
"addRequestId",
"captureUserId",
] as Middleware[]; // <-- Provide a list of middleware to call automatically
];
const withMiddleware = label(middleware, middlewareOrder);
@@ -0,0 +1,10 @@
import { BookingCancelService } from "@/lib/services/booking-cancel.service";
import { PrismaModule } from "@/modules/prisma/prisma.module";
import { Module } from "@nestjs/common";
@Module({
imports: [PrismaModule],
providers: [BookingCancelService],
exports: [BookingCancelService],
})
export class BookingCancelModule {}
@@ -0,0 +1,15 @@
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
import { BookingCancelService as BaseBookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking";
@Injectable()
export class BookingCancelService extends BaseBookingCancelService {
constructor(
prismaWriteService: PrismaWriteService,
) {
super({
prismaClient: prismaWriteService.prisma,
});
}
}
@@ -1,12 +1,7 @@
import { PrismaAttributeRepository } from "@/lib/repositories/prisma-attribute.repository";
import { PrismaBookingRepository } from "@/lib/repositories/prisma-booking.repository";
import { PrismaFeaturesRepository } from "@/lib/repositories/prisma-features.repository";
import { PrismaHostRepository } from "@/lib/repositories/prisma-host.repository";
import { PrismaOOORepository } from "@/lib/repositories/prisma-ooo.repository";
import { PrismaUserRepository } from "@/lib/repositories/prisma-user.repository";
import { CacheService } from "@/lib/services/cache.service";
import { CheckBookingAndDurationLimitsService } from "@/lib/services/check-booking-and-duration-limits.service";
import { CheckBookingLimitsService } from "@/lib/services/check-booking-limits.service";
import { LuckyUserService } from "@/lib/services/lucky-user.service";
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
import { Injectable } from "@nestjs/common";
@@ -21,26 +16,16 @@ export class RegularBookingService extends BaseRegularBookingService {
checkBookingAndDurationLimitsService: CheckBookingAndDurationLimitsService,
prismaWriteService: PrismaWriteService,
bookingRepository: PrismaBookingRepository,
featuresRepository: PrismaFeaturesRepository,
checkBookingLimitsService: CheckBookingLimitsService,
luckyUserService: LuckyUserService,
hostRepository: PrismaHostRepository,
oooRepository: PrismaOOORepository,
userRepository: PrismaUserRepository,
attributeRepository: PrismaAttributeRepository
userRepository: PrismaUserRepository
) {
super({
cacheService,
checkBookingAndDurationLimitsService,
prismaClient: prismaWriteService.prisma as unknown as PrismaClient,
bookingRepository,
featuresRepository,
checkBookingLimitsService,
luckyUserService,
hostRepository,
oooRepository,
userRepository,
attributeRepository,
});
}
}
@@ -404,7 +404,7 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
invalidWorkflow.steps = [
{
stepNumber: 1,
action: "sms_number",
action: "cal_ai_phone_call",
recipient: PHONE_NUMBER,
template: REMINDER,
verifiedPhoneId: verifiedPhoneId,
@@ -439,6 +439,18 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
html: "<p>Reminder for your event {EVENT_NAME}.</p>",
},
},
{
stepNumber: 2,
action: "sms_attendee",
recipient: EMAIL,
template: REMINDER,
phoneRequired: false,
sender: "updatedSender",
message: {
subject: "Update Upcoming: {EVENT_NAME}",
text: "Update Reminder for your event {EVENT_NAME}.</p>",
},
},
];
return request(app.getHttpServer())
.post(`${basePath}/routing-form`)
@@ -459,12 +471,15 @@ describe("OrganizationsTeamsWorkflowsController (E2E)", () => {
}
expect(responseBody.data.trigger.type).toEqual(sampleCreateWorkflowRoutingFormDto.trigger.type);
expect(responseBody.data.steps).toHaveLength(sampleCreateWorkflowRoutingFormDto.steps.length);
expect(responseBody.data.steps).toHaveLength(validWorkflow.steps.length);
expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.id).toBeDefined();
expect(responseBody.data.steps.find((step) => step.stepNumber === 1)?.sender).toEqual(
"CalcomE2EStep1"
);
expect(responseBody.data.steps.find((step) => step.stepNumber === 2)?.id).toBeDefined();
expect(responseBody.data.steps.find((step) => step.action === "sms_attendee")).toBeDefined();
const trigger = sampleCreateWorkflowRoutingFormDto.trigger as OnFormSubmittedTriggerDto;
expect(responseBody.data.trigger?.type).toEqual(trigger.type);
@@ -7,8 +7,12 @@ import {
EMAIL_ADDRESS,
EMAIL_ATTENDEE,
FORM_ALLOWED_STEP_ACTIONS,
SMS_NUMBER,
WorkflowEmailAddressStepDto,
WorkflowEmailAttendeeStepDto,
WorkflowPhoneNumberStepDto,
WorkflowPhoneAttendeeStepDto,
SMS_ATTENDEE,
} from "./workflow-step.input";
import {
RoutingFormWorkflowTriggerDto,
@@ -85,6 +89,8 @@ export class CreateFormWorkflowDto {
oneOf: [
{ $ref: getSchemaPath(WorkflowEmailAddressStepDto) },
{ $ref: getSchemaPath(WorkflowEmailAttendeeStepDto) },
{ $ref: getSchemaPath(WorkflowPhoneAttendeeStepDto) },
{ $ref: getSchemaPath(WorkflowPhoneNumberStepDto) },
],
type: "array",
})
@@ -99,8 +105,15 @@ export class CreateFormWorkflowDto {
subTypes: [
{ value: WorkflowEmailAddressStepDto, name: EMAIL_ADDRESS },
{ value: WorkflowEmailAttendeeStepDto, name: EMAIL_ATTENDEE },
{ value: WorkflowPhoneAttendeeStepDto, name: SMS_ATTENDEE },
{ value: WorkflowPhoneNumberStepDto, name: SMS_NUMBER },
],
},
})
steps!: (WorkflowEmailAddressStepDto | WorkflowEmailAttendeeStepDto)[];
steps!: (
| WorkflowEmailAddressStepDto
| WorkflowEmailAttendeeStepDto
| WorkflowPhoneAttendeeStepDto
| WorkflowPhoneNumberStepDto
)[];
}
@@ -8,6 +8,8 @@ import {
EMAIL_ADDRESS,
EMAIL_ATTENDEE,
FORM_ALLOWED_STEP_ACTIONS,
SMS_ATTENDEE,
SMS_NUMBER,
UpdateEmailAddressWorkflowStepDto,
UpdateEmailAttendeeWorkflowStepDto,
UpdateEmailHostWorkflowStepDto,
@@ -75,6 +77,8 @@ export class UpdateFormWorkflowDto {
oneOf: [
{ $ref: getSchemaPath(UpdateEmailAddressWorkflowStepDto) },
{ $ref: getSchemaPath(UpdateEmailAttendeeWorkflowStepDto) },
{ $ref: getSchemaPath(UpdatePhoneAttendeeWorkflowStepDto) },
{ $ref: getSchemaPath(UpdatePhoneNumberWorkflowStepDto) },
],
type: "array",
})
@@ -90,8 +94,15 @@ export class UpdateFormWorkflowDto {
subTypes: [
{ value: UpdateEmailAddressWorkflowStepDto, name: EMAIL_ADDRESS },
{ value: UpdateEmailAttendeeWorkflowStepDto, name: EMAIL_ATTENDEE },
{ value: UpdatePhoneAttendeeWorkflowStepDto, name: SMS_ATTENDEE },
{ value: UpdatePhoneNumberWorkflowStepDto, name: SMS_NUMBER },
],
},
})
steps?: (UpdateEmailAddressWorkflowStepDto | UpdateEmailAttendeeWorkflowStepDto)[];
steps?: (
| UpdateEmailAddressWorkflowStepDto
| UpdateEmailAttendeeWorkflowStepDto
| UpdatePhoneNumberWorkflowStepDto
| UpdatePhoneAttendeeWorkflowStepDto
)[];
}
@@ -24,7 +24,7 @@ export const STEP_ACTIONS = [
CAL_AI_PHONE_CALL,
] as const;
export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS] as const;
export const FORM_ALLOWED_STEP_ACTIONS = [EMAIL_ATTENDEE, EMAIL_ADDRESS, SMS_ATTENDEE, SMS_NUMBER] as const;
export const STEP_ACTIONS_TO_ENUM = {
[EMAIL_HOST]: WorkflowActions.EMAIL_HOST,
@@ -12,7 +12,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
import { validStatuses } from "~/bookings/lib/validStatuses";
import BookingsList from "~/bookings/views/bookings-listing-view";
import BookingsList from "~/bookings/views/bookings-view";
const querySchema = z.object({
status: z.enum(validStatuses),
+9
View File
@@ -26,11 +26,20 @@ async function handler(req: NextRequest) {
cookieStore.delete("calcom.csrf_token");
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
const result = await handleCancelBooking({
bookingData,
userId: session?.user?.id || -1,
});
// const bookingCancelService = getBookingCancelService();
// const result = await bookingCancelService.cancelBooking({
// bookingData: bookingData,
// bookingMeta: {
// userId: session?.user?.id || -1,
// },
// });
const statusCode = result.success ? 200 : 400;
return NextResponse.json(result, { status: statusCode });
-3
View File
@@ -1,3 +0,0 @@
import type { RegularBookingCreateResult } from "@calcom/features/bookings/lib/dto/types";
export type BookingResponse = RegularBookingCreateResult;
+2 -2
View File
@@ -11,11 +11,11 @@ import { z } from "zod";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { LastUsed, useLastUsed } from "@calcom/features/auth/lib/hooks/useLastUsed";
import { HOSTED_CAL_FEATURES, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { LastUsed, useLastUsed } from "@calcom/lib/hooks/useLastUsed";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTelemetry } from "@calcom/lib/hooks/useTelemetry";
import { collectPageParameters, telemetryEventTypes } from "@calcom/lib/telemetry";
@@ -62,7 +62,7 @@ export default function Login({
.string()
.min(1, `${t("error_required_field")}`)
.regex(emailRegex, `${t("enter_valid_email")}`),
...(!!totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
...(totpEmail ? {} : { password: z.string().min(1, `${t("error_required_field")}`) }),
})
// Passthrough other fields like totpCode
.passthrough();
@@ -0,0 +1,34 @@
"use client";
import type { Table as ReactTable } from "@tanstack/react-table";
import { DataTableFilters, DataTableSegment } from "@calcom/features/data-table";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import type { RowData, BookingListingStatus } from "../types";
type BookingsCalendarViewProps = {
status: BookingListingStatus;
table: ReactTable<RowData>;
};
export function BookingsCalendar({ table }: BookingsCalendarViewProps) {
return (
<>
<div className="mb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<DataTableFilters.FilterBar table={table} />
</div>
<div className="flex items-center gap-2">
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</div>
</div>
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen Icon="calendar" headline="Calendar view" description="Calendar view is coming soon." />
</div>
</>
);
}
@@ -0,0 +1,69 @@
"use client";
import type { Table as ReactTable } from "@tanstack/react-table";
import { DataTableWrapper, DataTableFilters, DataTableSegment } from "@calcom/features/data-table";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import type { RowData, BookingListingStatus } from "../types";
const descriptionByStatus: Record<BookingListingStatus, string> = {
upcoming: "upcoming_bookings",
recurring: "recurring_bookings",
past: "past_bookings",
cancelled: "cancelled_bookings",
unconfirmed: "unconfirmed_bookings",
};
type BookingsListViewProps = {
status: BookingListingStatus;
table: ReactTable<RowData>;
isPending: boolean;
totalRowCount?: number;
};
export function BookingsList({ status, table, isPending, totalRowCount }: BookingsListViewProps) {
const { t } = useLocale();
return (
<DataTableWrapper
className="mb-6"
table={table}
testId={`${status}-bookings`}
bodyTestId="bookings"
headerClassName="hidden"
isPending={isPending}
totalRowCount={totalRowCount}
variant="compact"
paginationMode="standard"
ToolbarLeft={
<>
<DataTableFilters.FilterBar table={table} />
</>
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
LoaderView={<SkeletonLoader />}
EmptyView={
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon="calendar"
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
}
/>
);
}
+25
View File
@@ -0,0 +1,25 @@
import type { RouterOutputs } from "@calcom/trpc/react";
import type { validStatuses } from "~/bookings/lib/validStatuses";
export type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
export type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
export type RowData =
| {
type: "data";
booking: BookingOutput;
isToday: boolean;
recurringInfo?: RecurringInfo;
}
| {
type: "today" | "next";
};
export type BookingListingStatus = (typeof validStatuses)[number];
@@ -121,13 +121,22 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
: eventTypeRaw.users;
if (!eventTypeRaw.users.length) {
if (!eventTypeRaw.owner)
return {
notFound: true,
} as const;
eventTypeRaw.users.push({
...eventTypeRaw.owner,
});
if (!eventTypeRaw.owner) {
if (bookingInfoRaw.user) {
eventTypeRaw.users.push({
...bookingInfoRaw.user,
hideBranding: false,
theme: null,
brandColor: null,
darkBrandColor: null,
isPlatformManaged: false,
});
} else {
return { notFound: true } as const;
}
} else {
eventTypeRaw.users.push({ ...eventTypeRaw.owner });
}
}
const eventType = {
@@ -2,57 +2,38 @@
import { useReactTable, getCoreRowModel, getSortedRowModel, createColumnHelper } from "@tanstack/react-table";
import { useSearchParams, usePathname } from "next/navigation";
import { useMemo, useRef } from "react";
import { createParser, useQueryState } from "nuqs";
import { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import {
useDataTable,
DataTableProvider,
DataTableWrapper,
DataTableFilters,
DataTableSegment,
type SystemFilterSegment,
useDataTable,
ColumnFilterType,
useFilterValue,
ZMultiSelectFilterValue,
ZDateRangeFilterValue,
ZTextFilterValue,
type SystemFilterSegment,
} from "@calcom/features/data-table";
import { useSegments } from "@calcom/features/data-table/hooks/useSegments";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Alert } from "@calcom/ui/components/alert";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import type { HorizontalTabItemProps } from "@calcom/ui/components/navigation";
import { HorizontalTabs } from "@calcom/ui/components/navigation";
import type { VerticalTabItemProps } from "@calcom/ui/components/navigation";
import { WipeMyCalActionButton } from "@calcom/web/components/apps/wipemycalother/wipeMyCalActionButton";
import BookingListItem from "@components/booking/BookingListItem";
import SkeletonLoader from "@components/booking/SkeletonLoader";
import { useFacetedUniqueValues } from "~/bookings/hooks/useFacetedUniqueValues";
import type { validStatuses } from "~/bookings/lib/validStatuses";
type BookingListingStatus = (typeof validStatuses)[number];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
recurringEventId: string | null;
count: number;
firstDate: Date | null;
bookings: { [key: string]: Date[] };
};
const descriptionByStatus: Record<BookingListingStatus, string> = {
upcoming: "upcoming_bookings",
recurring: "recurring_bookings",
past: "past_bookings",
cancelled: "cancelled_bookings",
unconfirmed: "unconfirmed_bookings",
};
import { BookingsCalendar } from "../components/BookingsCalendar";
import { BookingsList } from "../components/BookingsList";
import type { RowData, BookingOutput } from "../types";
type BookingsProps = {
status: (typeof validStatuses)[number];
@@ -103,21 +84,18 @@ export default function Bookings(props: BookingsProps) {
);
}
type RowData =
| {
type: "data";
booking: BookingOutput;
isToday: boolean;
recurringInfo?: RecurringInfo;
}
| {
type: "today" | "next";
};
const viewParser = createParser({
parse: (value: string) => {
if (value === "calendar") return "calendar";
return "list";
},
serialize: (value: "list" | "calendar") => value,
});
function BookingsContent({ status, permissions }: BookingsProps) {
const [view] = useQueryState("view", viewParser.withDefault("list"));
const { t } = useLocale();
const user = useMeQuery().data;
const tableContainerRef = useRef<HTMLDivElement>(null);
const searchParams = useSearchParams();
// Generate dynamic tabs that preserve query parameters
@@ -411,6 +389,9 @@ function BookingsContent({ status, permissions }: BookingsProps) {
getFacetedUniqueValues,
});
const isPending = query.isPending;
const totalRowCount = query.data?.totalCount;
return (
<div className="flex flex-col">
<div className="flex flex-row flex-wrap justify-between">
@@ -431,43 +412,16 @@ function BookingsContent({ status, permissions }: BookingsProps) {
{!!bookingsToday.length && status === "upcoming" && (
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
)}
<DataTableWrapper
className="mb-6"
tableContainerRef={tableContainerRef}
table={table}
testId={`${status}-bookings`}
bodyTestId="bookings"
headerClassName="hidden"
isPending={query.isPending}
totalRowCount={query.data?.totalCount}
variant="compact"
paginationMode="standard"
ToolbarLeft={
<>
<DataTableFilters.FilterBar table={table} />
</>
}
ToolbarRight={
<>
<DataTableFilters.ClearFiltersButton />
<DataTableSegment.SaveButton />
<DataTableSegment.Select />
</>
}
LoaderView={<SkeletonLoader />}
EmptyView={
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon="calendar"
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
description={t("no_status_bookings_yet_description", {
status: t(status).toLowerCase(),
description: t(descriptionByStatus[status]),
})}
/>
</div>
}
/>
{view === "list" ? (
<BookingsList
status={status}
table={table}
isPending={isPending}
totalRowCount={totalRowCount}
/>
) : (
<BookingsCalendar status={status} table={table} />
)}
</>
)}
</div>
+1 -2
View File
@@ -1,3 +1,4 @@
/* eslint-disable */
require("dotenv").config({ path: "../../.env" });
const englishTranslation = require("./public/static/locales/en/common.json");
const { withAxiom } = require("next-axiom");
@@ -223,8 +224,6 @@ const nextConfig = (phase) => {
"@calcom/dayjs",
"@calcom/emails",
"@calcom/embed-core",
"@calcom/embed-react",
"@calcom/embed-snippet",
"@calcom/features",
"@calcom/lib",
"@calcom/prisma",
+1
View File
@@ -201,6 +201,7 @@
"tailwindcss": "^3.3.3",
"tailwindcss-animate": "^1.0.6",
"ts-node": "^10.9.1",
"turbo": "^2.5.5",
"typescript": "^5.9.0-beta"
},
"nextBundleAnalysis": {
@@ -1460,6 +1460,7 @@
"email_host_action": "send email to host",
"email_attendee_action": "send email to attendees",
"email_attendee_action_form": "Send email to submitted email address",
"sms_attendee_action_form": "Send SMS to submitted phone number",
"sms_attendee_action": "Send SMS to attendee",
"sms_number_action": "send SMS to a specific number",
"send_reminder_sms": "Easily send meeting reminders via SMS to your attendees",
@@ -1893,7 +1894,7 @@
"only_show_first_available_slot_description": "This will limit your availability for this event type to one slot per day, scheduled at the earliest available time.",
"limit_total_booking_duration": "Limit total booking duration",
"limit_total_booking_duration_description": "Limit total amount of time that this event can be booked",
"booker_booking_limit": "Booker active booking limit",
"booker_upcoming_limit": "Limit number of upcoming bookings per booker",
"booker_booking_limit_description": "Limit the number of active bookings a booker can make for this event type",
"recurring_event_doesnt_support_booker_booking_limit": "Recurring events don't support booker booking limit",
"add_limit": "Add Limit",
@@ -3636,8 +3637,8 @@
"error_downloading_expense_log": "Error downloading expense log",
"require_booker_email_verification": "Prevent Impersonation on Bookings",
"require_booker_email_verification_description": "When enabled, anyone trying to book events using your email address must verify they own it via a one time code or be logged in to prevent impersonation",
"offer_to_reschedule_last_booking": "Offer to reschedule last active booking to chosen time slot",
"booker_limit_exceeded_error": "Booker maximum active booking limit exceeded",
"reschedule_last_booking_offer": "Offer to reschedule the last booking to the new time slot",
"booker_upcoming_limit_reached": "You've reached the limit of {{count}} upcoming bookings for this event type",
"booker_limit_exceeded_error_reschedule": "You already have a booking for this event on {{date}}. Would you like to reschedule to the new selected time?",
"duration_min_error": "Duration must be at least {{min}} minute(s)",
"duration_max_error": "Duration cannot exceed {{max}} minutes (24 hours)",
@@ -1,9 +1,6 @@
import { createMocks } from "node-mocks-http";
import type {
CustomNextApiRequest,
CustomNextApiResponse,
} from "@calcom/features/bookings/lib/handleNewBooking/test/fresh-booking.test";
import type { CustomNextApiRequest, CustomNextApiResponse } from "./types";
export function createMockNextJsRequest(...args: Parameters<typeof createMocks>) {
return createMocks<CustomNextApiRequest, CustomNextApiResponse>(...args);
+6
View File
@@ -0,0 +1,6 @@
import type { Request, Response } from "express";
import type { NextApiRequest, NextApiResponse } from "next";
export type CustomNextApiRequest = NextApiRequest & Request;
export type CustomNextApiResponse = NextApiResponse & Response;
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 161 KiB

+1
View File
@@ -140,6 +140,7 @@
"self-hosting/deployments/azure",
"self-hosting/deployments/elestio",
"self-hosting/deployments/gcp",
"self-hosting/deployments/northflank",
"self-hosting/deployments/railway",
"self-hosting/deployments/render",
"self-hosting/deployments/vercel"
@@ -0,0 +1,13 @@
---
title: "Northflank"
icon: "square-n"
---
You can deploy Cal.com on Northflank using the button below.
The team at Northflank also have a [detailed blog post](https://northflank.com/guides/deploy-calcom-with-northflank) on deploying Cal.com on their platform.
## One Click Deployment
<a href="https://northflank.com/stacks/deploy-calcom" target="_blank">
<img src="/images/deploy-northflank.svg" noZoom alt="Deploy on Northflank" />
</a>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "calcom-monorepo",
"version": "0.0.0",
"version": "5.8.2",
"private": true,
"workspaces": [
"apps/*",
@@ -0,0 +1,12 @@
import type { TFunction } from "i18next";
export interface IBookingRedirect {
language: TFunction;
fromEmail: string;
eventOwner: string;
toEmail: string;
toName: string;
oldDates?: string;
dates: string;
action: "add" | "update" | "cancel";
}
+23
View File
@@ -0,0 +1,23 @@
import type { TFunction } from "i18next";
export type OrganizationCreation = {
language: TFunction;
from: string;
to: string;
ownerNewUsername: string;
ownerOldUsername: string | null;
orgDomain: string;
orgName: string;
prevLink: string | null;
newLink: string;
};
export type EmailVerifyCode = {
language: TFunction;
user: {
name?: string | null;
email: string;
};
verificationEmailCode: string;
isVerifyingEmail?: boolean;
};
@@ -0,0 +1,26 @@
import dayjs from "@calcom/dayjs";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
export function getFormattedDate(calEvent: CalendarEvent, attendee: Person): string {
const inviteeTimeFormat = calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR;
const timezone = attendee.timeZone;
const locale = attendee.language.locale;
const t = attendee.language.translate;
const getFormattedRecipientTime = (time: string, format: string) => {
return dayjs(time).tz(timezone).locale(locale).format(format);
};
const getInviteeStart = (format: string) => {
return getFormattedRecipientTime(calEvent.startTime, format);
};
const getInviteeEnd = (format: string) => {
return getFormattedRecipientTime(calEvent.endTime, format);
};
return `${getInviteeStart(inviteeTimeFormat)} - ${getInviteeEnd(inviteeTimeFormat)}, ${t(
getInviteeStart("dddd").toLowerCase()
)}, ${t(getInviteeStart("MMMM").toLowerCase())} ${getInviteeStart("D, YYYY")}`;
}
@@ -0,0 +1,59 @@
import type { TFunction } from "i18next";
import { APP_NAME } from "@calcom/lib/constants";
export type TeamInvite = {
language: TFunction;
from: string;
to: string;
teamName: string;
joinLink: string;
isCalcomMember: boolean;
/**
* We ideally should have a separate email for auto-join(when a user is automatically accepted into a team/org), but we don't have one yet.
*/
isAutoJoin: boolean;
isOrg: boolean;
parentTeamName: string | undefined;
isExistingUserMovedToOrg: boolean;
prevLink: string | null;
newLink: string | null;
};
export function getTypeOfInvite(teamInviteEvent: TeamInvite) {
if (teamInviteEvent.isOrg) {
return "TO_ORG";
}
if (teamInviteEvent.parentTeamName) {
return "TO_SUBTEAM";
}
if (teamInviteEvent.isAutoJoin) {
throw new Error("Auto-join is not supported for regular teams");
}
return "TO_REGULAR_TEAM";
}
export const getSubject = (teamInviteEvent: TeamInvite) => {
const typeOfInvite = getTypeOfInvite(teamInviteEvent);
const type = teamInviteEvent.isAutoJoin ? "added" : "invited";
const variables = {
user: teamInviteEvent.from,
team: teamInviteEvent.teamName,
appName: APP_NAME,
parentTeamName: teamInviteEvent.parentTeamName,
entity: teamInviteEvent.language(teamInviteEvent.isOrg ? "organization" : "team").toLowerCase(),
};
if (typeOfInvite === "TO_ORG") {
return teamInviteEvent.language(`email_team_invite|subject|${type}_to_org`, variables);
}
if (typeOfInvite === "TO_SUBTEAM") {
return teamInviteEvent.language(`email_team_invite|subject|${type}_to_subteam`, variables);
}
return teamInviteEvent.language(`email_team_invite|subject|${type}_to_regular_team`, variables);
};
@@ -1,8 +1,8 @@
import AttendeeScheduledEmailClass from "../../templates/attendee-rescheduled-email";
import { getFormattedDate } from "../../lib/utils/date-formatting";
import { AttendeeScheduledEmail } from "./AttendeeScheduledEmail";
export const AttendeeRequestEmail = (props: React.ComponentProps<typeof AttendeeScheduledEmail>) => {
const date = new AttendeeScheduledEmailClass(props.calEvent, props.attendee).getFormattedDate();
const date = getFormattedDate(props.calEvent, props.attendee);
return (
<AttendeeScheduledEmail
@@ -1,4 +1,4 @@
import type { IBookingRedirect } from "../../templates/booking-redirect-notification";
import type { IBookingRedirect } from "../../lib/types/booking-redirect-types";
import { BaseEmailHtml } from "../components";
export const BookingRedirectEmailNotification = (
@@ -1,7 +1,7 @@
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import type { OrganizationCreation } from "../../templates/organization-creation-email";
import type { OrganizationCreation } from "../../lib/types/email-types";
import { V2BaseEmailHtml } from "../components";
export const OrganizationCreationEmail = (
@@ -3,7 +3,7 @@ import type { TFunction } from "i18next";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { APP_NAME, WEBAPP_URL, IS_PRODUCTION } from "@calcom/lib/constants";
import { getSubject, getTypeOfInvite } from "../../templates/team-invite-email";
import { getSubject, getTypeOfInvite } from "../../lib/utils/team-invite-utils";
import { V2BaseEmailHtml, CallToAction } from "../components";
type TeamInvite = {
@@ -1,6 +1,6 @@
import { APP_NAME, SENDER_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
import type { EmailVerifyCode } from "../../templates/attendee-verify-email";
import type { EmailVerifyCode } from "../../lib/types/email-types";
import { BaseEmailHtml } from "../components";
export const VerifyEmailByCode = (
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { APP_NAME, COMPANY_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type EmailVerifyLink = {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type OrganizationNotification = {
@@ -1,7 +1,7 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
@@ -1,6 +1,6 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail {
@@ -1,8 +1,8 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
@@ -1,6 +1,6 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail {
@@ -5,7 +5,7 @@ import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail {
@@ -4,7 +4,7 @@ import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail {
@@ -1,7 +1,7 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail {
@@ -1,7 +1,7 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
@@ -2,7 +2,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeRequestEmail extends AttendeeScheduledEmail {
@@ -1,8 +1,8 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
@@ -6,8 +6,8 @@ import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class AttendeeScheduledEmail extends BaseEmail {
@@ -1,8 +1,8 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class AttendeeUpdatedEmail extends AttendeeScheduledEmail {
@@ -1,19 +1,10 @@
import type { TFunction } from "i18next";
import { APP_NAME, COMPANY_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import type { EmailVerifyCode } from "../lib/types/email-types";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type EmailVerifyCode = {
language: TFunction;
user: {
name?: string | null;
email: string;
};
verificationEmailCode: string;
isVerifyingEmail?: boolean;
};
export type { EmailVerifyCode } from "../lib/types/email-types";
export default class AttendeeVerifyEmail extends BaseEmail {
verifyAccountInput: EmailVerifyCode;
@@ -2,8 +2,8 @@ import { getManageLink } from "@calcom/lib/CalEventParser";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@@ -1,20 +1,10 @@
import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "..";
import type { IBookingRedirect } from "../lib/types/booking-redirect-types";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export interface IBookingRedirect {
language: TFunction;
fromEmail: string;
eventOwner: string;
toEmail: string;
toName: string;
oldDates?: string;
dates: string;
action: "add" | "update" | "cancel";
}
export type { IBookingRedirect } from "../lib/types/booking-redirect-types";
export default class BookingRedirectNotification extends BaseEmail {
bookingRedirect: IBookingRedirect;
@@ -5,7 +5,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class BrokenIntegrationEmail extends BaseEmail {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { APP_NAME, COMPANY_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type ChangeOfEmailVerifyLink = {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class CreditBalanceLimitReachedEmail extends BaseEmail {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class CreditBalanceLowWarningEmail extends BaseEmail {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class DisabledAppEmail extends BaseEmail {
+1 -1
View File
@@ -1,6 +1,6 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export interface Feedback {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { APP_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type PasswordReset = {
@@ -1,6 +1,6 @@
import { APP_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import type { MonthlyDigestEmailData } from "../src/templates/MonthlyDigestEmail";
import BaseEmail from "./_base-email";
@@ -1,6 +1,6 @@
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import AttendeeScheduledEmail from "./attendee-scheduled-email";
export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail {
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { APP_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type OrgAutoInvite = {
@@ -1,21 +1,10 @@
import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import type { OrganizationCreation } from "../lib/types/email-types";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type OrganizationCreation = {
language: TFunction;
from: string;
to: string;
ownerNewUsername: string;
ownerOldUsername: string | null;
orgDomain: string;
orgName: string;
prevLink: string | null;
newLink: string;
};
export type { OrganizationCreation } from "../lib/types/email-types";
export default class OrganizationCreationEmail extends BaseEmail {
organizationCreationEvent: OrganizationCreation;
@@ -1,8 +1,8 @@
import { APP_NAME } from "@calcom/lib/constants";
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
@@ -1,6 +1,6 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
@@ -1,8 +1,8 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
import type { Reassigned } from "./organizer-scheduled-email";
@@ -5,7 +5,7 @@ import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail {
@@ -5,7 +5,7 @@ import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class OrganizerDailyVideoDownloadTranscriptEmail extends BaseEmail {
@@ -1,8 +1,8 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
@@ -1,6 +1,6 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerPaymentRefundFailedEmail extends OrganizerScheduledEmail {
@@ -1,8 +1,8 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
import type { Reassigned } from "./organizer-scheduled-email";
@@ -3,7 +3,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
/**
@@ -1,7 +1,7 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { renderEmail } from "../";
import renderEmail from "../src/renderEmail";
import OrganizerRequestEmail from "./organizer-request-email";
export default class OrganizerRequestReminderEmail extends OrganizerRequestEmail {
@@ -1,8 +1,8 @@
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { renderEmail } from "..";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail {
@@ -2,8 +2,8 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import OrganizerScheduledEmail from "./organizer-scheduled-email";
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
@@ -7,9 +7,9 @@ import { getReplyToHeader } from "@calcom/lib/getReplyToHeader";
import { TimeFormat } from "@calcom/lib/timeFormat";
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import { renderEmail } from "../";
import generateIcsFile from "../lib/generateIcsFile";
import { GenerateIcsRole } from "../lib/generateIcsFile";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type Reassigned = { name: string | null; email: string; reason?: string; byUser?: string };
@@ -2,7 +2,7 @@ import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "..";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export default class SlugReplacementEmail extends BaseEmail {
+4 -59
View File
@@ -1,65 +1,10 @@
import type { TFunction } from "i18next";
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { APP_NAME, EMAIL_FROM_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import { getSubject, type TeamInvite } from "../lib/utils/team-invite-utils";
import renderEmail from "../src/renderEmail";
import BaseEmail from "./_base-email";
export type TeamInvite = {
language: TFunction;
from: string;
to: string;
teamName: string;
joinLink: string;
isCalcomMember: boolean;
/**
* We ideally should have a separate email for auto-join(when a user is automatically accepted into a team/org), but we don't have one yet.
*/
isAutoJoin: boolean;
isOrg: boolean;
parentTeamName: string | undefined;
isExistingUserMovedToOrg: boolean;
prevLink: string | null;
newLink: string | null;
};
export function getTypeOfInvite(teamInviteEvent: TeamInvite) {
if (teamInviteEvent.isOrg) {
return "TO_ORG";
}
if (teamInviteEvent.parentTeamName) {
return "TO_SUBTEAM";
}
if (teamInviteEvent.isAutoJoin) {
throw new Error("Auto-join is not supported for regular teams");
}
return "TO_REGULAR_TEAM";
}
export const getSubject = (teamInviteEvent: TeamInvite) => {
const typeOfInvite = getTypeOfInvite(teamInviteEvent);
const type = teamInviteEvent.isAutoJoin ? "added" : "invited";
const variables = {
user: teamInviteEvent.from,
team: teamInviteEvent.teamName,
appName: APP_NAME,
parentTeamName: teamInviteEvent.parentTeamName,
entity: teamInviteEvent.language(teamInviteEvent.isOrg ? "organization" : "team").toLowerCase(),
};
if (typeOfInvite === "TO_ORG") {
return teamInviteEvent.language(`email_team_invite|subject|${type}_to_org`, variables);
}
if (typeOfInvite === "TO_SUBTEAM") {
return teamInviteEvent.language(`email_team_invite|subject|${type}_to_subteam`, variables);
}
return teamInviteEvent.language(`email_team_invite|subject|${type}_to_regular_team`, variables);
};
export { getSubject, getTypeOfInvite, type TeamInvite } from "../lib/utils/team-invite-utils";
export default class TeamInviteEmail extends BaseEmail {
teamInviteEvent: TeamInvite;
+1 -1
View File
@@ -3,9 +3,9 @@ import type { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import z from "zod";
import { LastUsed, useLastUsed } from "@calcom/features/auth/lib/hooks/useLastUsed";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { emailRegex } from "@calcom/lib/emailSchema";
import { LastUsed, useLastUsed } from "@calcom/lib/hooks/useLastUsed";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { ButtonProps } from "@calcom/ui/components/button";
@@ -301,6 +301,7 @@ const getError = ({
const error = dataError;
let date = "";
let count = 0;
if (error.message === ErrorCode.BookerLimitExceededReschedule) {
const formattedDate = formatEventFromTime({
@@ -312,9 +313,16 @@ const getError = ({
date = `${formattedDate.date} ${formattedDate.time}`;
}
if (error.message === ErrorCode.BookerLimitExceeded && error.data?.count) {
count = error.data.count;
}
const messageKey =
error.message === ErrorCode.BookerLimitExceeded ? "booker_upcoming_limit_reached" : error.message;
return error?.message ? (
<>
{responseVercelIdHeader ?? ""} {t(error.message, { date })}
{responseVercelIdHeader ?? ""} {t(messageKey, { date, count })}
</>
) : (
<>{t("can_you_try_again")}</>
@@ -3,6 +3,7 @@ import { useMemo, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { useBookerStoreContext } from "@calcom/features/bookings/Booker/BookerStoreProvider";
import { useAvailableTimeSlots } from "@calcom/features/bookings/Booker/components/hooks/useAvailableTimeSlots";
import { useBookerTime } from "@calcom/features/bookings/Booker/components/hooks/useBookerTime";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { Calendar } from "@calcom/features/calendars/weeklyview";
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
@@ -31,6 +32,7 @@ export const LargeCalendar = ({
const overlayEvents = useOverlayCalendarStore((state) => state.overlayBusyDates);
const displayOverlay =
getQueryParam("overlayCalendar") === "true" || localStorage?.getItem("overlayCalendarSwitchDefault");
const { timezone } = useBookerTime();
const eventDuration = selectedEventDuration || event?.data?.length || 30;
@@ -43,7 +45,6 @@ export const LargeCalendar = ({
// HACK: force rerender when overlay events change
// Sine we dont use react router here we need to force rerender (ATOM SUPPORT)
// eslint-disable-next-line @typescript-eslint/no-empty-function
useEffect(() => {}, [displayOverlay]);
const overlayEventsForDate = useMemo(() => {
@@ -75,6 +76,7 @@ export const LargeCalendar = ({
gridCellsPerHour={60 / eventDuration}
hoverEventDuration={eventDuration}
hideHeader
timezone={timezone}
/>
</div>
);
@@ -0,0 +1,14 @@
import { createContainer } from "@calcom/lib/di/di";
import {
type BookingCancelService,
moduleLoader as bookingCancelServiceModule,
} from "../modules/BookingCancelService.module";
const bookingCancelServiceContainer = createContainer();
export function getBookingCancelService(): BookingCancelService {
bookingCancelServiceModule.loadModule(bookingCancelServiceContainer);
return bookingCancelServiceContainer.get<BookingCancelService>(bookingCancelServiceModule.token);
}
@@ -0,0 +1,24 @@
import { BookingCancelService } from "@calcom/features/bookings/lib/handleCancelBooking";
import { bindModuleToClassOnToken, createModule } from "@calcom/lib/di/di";
import { DI_TOKENS } from "@calcom/lib/di/tokens";
import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module";
const thisModule = createModule();
const token = DI_TOKENS.BOOKING_CANCEL_SERVICE;
const moduleToken = DI_TOKENS.BOOKING_CANCEL_SERVICE_MODULE;
const loadModule = bindModuleToClassOnToken({
module: thisModule,
moduleToken,
token,
classs: BookingCancelService,
depsMap: {
prismaClient: prismaModuleLoader,
},
});
export const moduleLoader = {
token,
loadModule,
};
export type { BookingCancelService };
@@ -1,6 +1,7 @@
import { InstantBookingCreateService } from "@calcom/features/bookings/lib/service/InstantBookingCreateService";
import { createModule, bindModuleToClassOnToken } from "@calcom/features/di/di";
import { DI_TOKENS } from "@calcom/features/di/tokens";
import { InstantBookingCreateService } from "@calcom/features/instant-meeting/handleInstantMeeting";
import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module";
export const instantBookingCreateServiceModule = createModule();
const token = DI_TOKENS.INSTANT_BOOKING_CREATE_SERVICE;
@@ -10,7 +11,10 @@ const loadModule = bindModuleToClassOnToken({
moduleToken,
token,
classs: InstantBookingCreateService,
depsMap: {},
depsMap: {
// TODO: In a followup PR, we aim to remove prisma dependency and instead inject the repositories as dependencies.
prismaClient: prismaModuleLoader,
},
});
export type { InstantBookingCreateService };
@@ -1,4 +1,5 @@
import { RecurringBookingService } from "@calcom/features/bookings/lib/handleNewRecurringBooking";
import { RecurringBookingService } from "@calcom/features/bookings/lib/service/RecurringBookingService";
import { createModule, bindModuleToClassOnToken } from "@calcom/features/di/di";
import { DI_TOKENS } from "@calcom/features/di/tokens";
@@ -1,14 +1,11 @@
import { RegularBookingService } from "@calcom/features/bookings/lib/handleNewBooking";
import { RegularBookingService } from "@calcom/features/bookings/lib/service/RegularBookingService";
import { bindModuleToClassOnToken, createModule } from "@calcom/features/di/di";
import { moduleLoader as attributeRepositoryModuleLoader } from "@calcom/features/di/modules/Attribute";
import { moduleLoader as bookingRepositoryModuleLoader } from "@calcom/features/di/modules/Booking";
import { moduleLoader as cacheModuleLoader } from "@calcom/features/di/modules/Cache";
import { moduleLoader as checkBookingAndDurationLimitsModuleLoader } from "@calcom/features/di/modules/CheckBookingAndDurationLimits";
import { moduleLoader as checkBookingLimitsModuleLoader } from "@calcom/features/di/modules/CheckBookingLimits";
import { moduleLoader as featuresRepositoryModuleLoader } from "@calcom/features/di/modules/Features";
import { moduleLoader as hostRepositoryModuleLoader } from "@calcom/features/di/modules/Host";
import { moduleLoader as luckyUserServiceModuleLoader } from "@calcom/features/di/modules/LuckyUser";
import { moduleLoader as oooRepositoryModuleLoader } from "@calcom/features/di/modules/Ooo";
import { moduleLoader as userRepositoryModuleLoader } from "@calcom/features/di/modules/User";
import { DI_TOKENS } from "@calcom/features/di/tokens";
import { moduleLoader as prismaModuleLoader } from "@calcom/prisma/prisma.module";
@@ -22,17 +19,13 @@ const loadModule = bindModuleToClassOnToken({
token,
classs: RegularBookingService,
depsMap: {
// TODO: In a followup PR, we aim to remove prisma dependency and instead inject the repositories as dependencies.
prismaClient: prismaModuleLoader,
cacheService: cacheModuleLoader,
checkBookingAndDurationLimitsService: checkBookingAndDurationLimitsModuleLoader,
prismaClient: prismaModuleLoader,
bookingRepository: bookingRepositoryModuleLoader,
featuresRepository: featuresRepositoryModuleLoader,
checkBookingLimitsService: checkBookingLimitsModuleLoader,
luckyUserService: luckyUserServiceModuleLoader,
hostRepository: hostRepositoryModuleLoader,
oooRepository: oooRepositoryModuleLoader,
userRepository: userRepositoryModuleLoader,
attributeRepository: attributeRepositoryModuleLoader,
},
});
+2
View File
@@ -5,4 +5,6 @@ export const BOOKING_DI_TOKENS = {
RECURRING_BOOKING_SERVICE_MODULE: Symbol("RecurringBookingServiceModule"),
INSTANT_BOOKING_CREATE_SERVICE: Symbol("InstantBookingCreateService"),
INSTANT_BOOKING_CREATE_SERVICE_MODULE: Symbol("InstantBookingCreateServiceModule"),
BOOKING_CANCEL_SERVICE: Symbol("BookingCancelService"),
BOOKING_CANCEL_SERVICE_MODULE: Symbol("BookingCancelServiceModule"),
};
@@ -6,8 +6,8 @@ import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/get
import { parseRecurringDates } from "@calcom/lib/parse-dates";
import type { RoutingFormSearchParams } from "@calcom/platform-types";
import type { BookerEvent, BookingCreateBody, RecurringBookingCreateBody } from "../../types";
import type { Tracking } from "../handleNewBooking/types";
import type { BookerEvent, BookingCreateBody, RecurringBookingCreateBody } from "../../../types";
import type { Tracking } from "../../handleNewBooking/types";
export type BookingOptions = {
values: Record<string, unknown>;
+35
View File
@@ -0,0 +1,35 @@
/**
* Domain types for Booking Cancellation Service
* These types are framework-agnostic and contain only the data required for booking cancellation operations
*/
import type { z } from "zod";
import type { bookingCancelInput } from "@calcom/prisma/zod-utils";
/**
* The booking data required for cancellation
*/
export type CancelRegularBookingData = z.infer<typeof bookingCancelInput>;
/**
* Additional metadata for booking cancellation, typically used for platform integrations
*/
export type CancelBookingMeta = {
userId?: number;
platformClientId?: string;
platformRescheduleUrl?: string;
platformCancelUrl?: string;
platformBookingUrl?: string;
arePlatformEmailsEnabled?: boolean;
};
/**
* The response returned after attempting to cancel a booking
*/
export type HandleCancelBookingResponse = {
success: boolean;
message: string;
onlyRemovedAttendee: boolean;
bookingId: number;
bookingUid: string;
};
@@ -0,0 +1,10 @@
import { z } from "zod";
export const instantMeetingSubscriptionSchema = z.object({
endpoint: z.string(),
expirationTime: z.any().optional(),
keys: z.object({
auth: z.string(),
p256dh: z.string(),
}),
});
+5 -6
View File
@@ -7,9 +7,8 @@ import type getBookingDataSchemaForApi from "@calcom/features/bookings/lib/getBo
import type { SchedulingType } from "@calcom/prisma/enums";
import type { ExtendedBookingCreateBody } from "../bookingCreateBodySchema";
import type { RegularBookingService } from "../handleNewBooking";
import type { RegularBookingService } from "../service/RegularBookingService";
export type { BookingCreateBody } from "../bookingCreateBodySchema";
export type BookingDataSchemaGetter = typeof getBookingDataSchema | typeof getBookingDataSchemaForApi;
export type CreateRegularBookingData = ExtendedBookingCreateBody;
@@ -44,14 +43,14 @@ export type BookingHandlerInput = {
bookingData: CreateRegularBookingData;
} & CreateBookingMeta;
// TODO: In a followup PR, we working on defining the type here itself instead of inferring it.
export type RegularBookingCreateResult = Awaited<ReturnType<RegularBookingService["createBooking"]>>;
export type InstantBookingCreateResult = {
message: string;
message: "Success";
meetingTokenId: number;
bookingId: number;
bookingUid: string;
expires: Date;
userId: number | null;
};
// TODO: In a followup PR, we working on defining the type here itself instead of inferring it.
export type RegularBookingCreateResult = Awaited<ReturnType<RegularBookingService["createBooking"]>>;
@@ -5,10 +5,13 @@ import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApi
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmailsAndSMS } from "@calcom/emails";
import EventManager from "@calcom/features/bookings/lib/EventManager";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/payment/processNoShowFeeOnCancellation";
import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import {
@@ -17,8 +20,6 @@ import {
} from "@calcom/features/webhooks/lib/scheduleTrigger";
import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import EventManager from "@calcom/features/bookings/lib/EventManager";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import { HttpError } from "@calcom/lib/http-error";
@@ -27,10 +28,10 @@ import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
// TODO: Prisma import would be used from DI in a followup PR when we remove `handler` export
import prisma from "@calcom/prisma";
import type { Prisma, WorkflowReminder } from "@calcom/prisma/client";
import type { Prisma, PrismaClient, WorkflowReminder } from "@calcom/prisma/client";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
import { BookingStatus } from "@calcom/prisma/enums";
import { bookingMetadataSchema, bookingCancelInput } from "@calcom/prisma/zod-utils";
@@ -38,10 +39,16 @@ import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type {
CancelRegularBookingData,
CancelBookingMeta,
HandleCancelBookingResponse,
} from "./dto/BookingCancel";
import { getAllCredentialsIncludeServiceAccountKey } from "./getAllCredentialsForUsersOnEvent/getAllCredentials";
import { getBookingToDelete } from "./getBookingToDelete";
import { handleInternalNote } from "./handleInternalNote";
import cancelAttendeeSeat from "./handleSeats/cancel/cancelAttendeeSeat";
import type { IBookingCancelService } from "./interfaces/IBookingCancelService";
const log = logger.getSubLogger({ prefix: ["handleCancelBooking"] });
@@ -60,14 +67,6 @@ export type CancelBookingInput = {
bookingData: z.infer<typeof bookingCancelInput>;
} & PlatformParams;
export type HandleCancelBookingResponse = {
success: boolean;
message: string;
onlyRemovedAttendee: boolean;
bookingId: number;
bookingUid: string;
};
async function handler(input: CancelBookingInput) {
const body = input.bookingData;
const {
@@ -118,7 +117,12 @@ async function handler(input: CancelBookingInput) {
const isCancellationUserHost =
bookingToDelete.userId == userId || bookingToDelete.user.email === cancelledBy;
if (!platformClientId && !cancellationReason?.trim() && isCancellationUserHost && !skipCancellationReasonValidation) {
if (
!platformClientId &&
!cancellationReason?.trim() &&
isCancellationUserHost &&
!skipCancellationReasonValidation
) {
throw new HttpError({
statusCode: 400,
message: "Cancellation reason is required when you are the host",
@@ -588,4 +592,24 @@ async function handler(input: CancelBookingInput) {
} satisfies HandleCancelBookingResponse;
}
type BookingCancelServiceDependencies = {
prismaClient: PrismaClient;
};
/**
* Takes care of cancelling bookings. This includes regular bookings, recurring bookings, seated bookings, etc.
* Handles both individual booking cancellations and bulk cancellations for recurring events.
*/
export class BookingCancelService implements IBookingCancelService {
constructor(private readonly deps: BookingCancelServiceDependencies) {}
async cancelBooking(input: { bookingData: CancelRegularBookingData; bookingMeta?: CancelBookingMeta }) {
const cancelBookingInput: CancelBookingInput = {
bookingData: input.bookingData,
...(input.bookingMeta || {}),
};
// TODO: Deps to be passed to it later when we stop exporting handler
return handler(cancelBookingInput);
}
}
export default handler;
@@ -61,7 +61,9 @@ const checkActiveBookingsLimit = async ({
if (bookingsCount >= maxActiveBookingsPerBooker) {
log.warn(`Maximum booking limit reached for ${bookerEmail} for event type ${eventTypeId}`);
throw new ErrorWithCode(ErrorCode.BookerLimitExceeded, ErrorCode.BookerLimitExceeded);
throw new ErrorWithCode(ErrorCode.BookerLimitExceeded, ErrorCode.BookerLimitExceeded, {
count: maxActiveBookingsPerBooker,
});
}
};
@@ -2,7 +2,7 @@ import { describe, it, expect, vi } from "vitest";
import { BookingStatus, CreationSource } from "@calcom/prisma/enums";
import { buildDryRunBooking } from "../../handleNewBooking";
import { buildDryRunBooking } from "../../service/RegularBookingService";
vi.mock("@calcom/prisma", () => ({
default: {}, // empty object as default export
@@ -3,7 +3,7 @@ import { vi, describe, it, expect, beforeEach } from "vitest";
import { SchedulingType } from "@calcom/prisma/enums";
import { buildEventForTeamEventType } from "../../handleNewBooking";
import { buildEventForTeamEventType } from "../../service/RegularBookingService";
vi.mock("@calcom/lib/server/i18n", () => ({
getTranslation: vi.fn().mockResolvedValue("translated"),
@@ -22,12 +22,12 @@ const withDestinationCalendarSpy = vi.fn().mockReturnThis();
vi.mock("@calcom/features/CalendarEventBuilder", () => {
return {
CalendarEventBuilder: {
fromEvent: vi.fn().mockImplementation((evt) => ({
fromEvent: vi.fn().mockImplementation((_evt) => ({
withDestinationCalendar: withDestinationCalendarSpy,
withTeam: withTeamSpy,
build: vi.fn().mockImplementation(() => ({
destinationCalendar: [],
team: {}, // <- you won’t use this result anyway
team: {}, // <- you won't use this result anyway
})),
})),
},
@@ -72,7 +72,7 @@ describe("buildEventForTeamEventType", () => {
});
it("filters out the organizer", async () => {
const result = await buildEventForTeamEventType({
await buildEventForTeamEventType({
existingEvent: {},
users: [baseUser({ email: "organizer@example.com" })],
organizerUser: { email: "organizer@example.com" },
@@ -80,7 +80,7 @@ describe("buildEventForTeamEventType", () => {
});
const teamArgs = withTeamSpy.mock.calls[0][0];
const memberEmails = teamArgs.members.map((m: any) => m.email);
const memberEmails = teamArgs.members.map((m: { email: string }) => m.email);
expect(memberEmails).not.toContain("organizer@example.com");
});
@@ -123,7 +123,7 @@ describe("buildEventForTeamEventType", () => {
});
const teamArgs = withTeamSpy.mock.calls[0][0];
const memberEmails = teamArgs.members.map((m: any) => m.email);
const memberEmails = teamArgs.members.map((m: { email: string }) => m.email);
expect(memberEmails).toContain("fixed@example.com");
expect(memberEmails).toContain("nonfixed@example.com");
@@ -142,7 +142,7 @@ describe("buildEventForTeamEventType", () => {
});
const teamArgs = withTeamSpy.mock.calls[0][0];
const memberEmails = teamArgs.members.map((m: any) => m.email);
const memberEmails = teamArgs.members.map((m: { email: string }) => m.email);
expect(memberEmails).toContain("fixed@example.com");
expect(memberEmails).toContain("nonfixed1@example.com");

Some files were not shown because too many files have changed in this diff Show More