Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
685eadc30b | ||
|
|
c821f15a14 | ||
|
|
8a972a74c6 | ||
|
|
cf76cf7aae | ||
|
|
4dd762681c | ||
|
|
399a23a52f | ||
|
|
342f935aba | ||
|
|
27e4210693 | ||
|
|
66cb2bcc13 | ||
|
|
30c31d3585 | ||
|
|
e17303ea88 | ||
|
|
bebf1d80a7 | ||
|
|
5bcefb0b3e | ||
|
|
f52de7331f | ||
|
|
1690770d6a | ||
|
|
7759e6a029 |
No files matched your search
@@ -34,6 +34,11 @@ jobs:
|
|||||||
uses: ./.github/workflows/api-v2-production-build.yml
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-atoms:
|
||||||
|
name: Production builds
|
||||||
|
uses: ./.github/workflows/atoms-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
uses: ./.github/workflows/production-build-without-database.yml
|
uses: ./.github/workflows/production-build-without-database.yml
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: Atoms production Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build atoms
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
|
- uses: ./.github/actions/yarn-install
|
||||||
|
- name: Cache atoms production build
|
||||||
|
uses: buildjet/cache@v4
|
||||||
|
id: cache-atoms-build
|
||||||
|
env:
|
||||||
|
cache-name: atoms-build
|
||||||
|
key-1: ${{ hashFiles('yarn.lock') }}
|
||||||
|
key-2: ${{ hashFiles('packages/platform/atoms/**.[jt]s', 'packages/platform/atoms/**.[jt]sx', '!**/node_modules') }}
|
||||||
|
key-3: ${{ github.event.pull_request.number || github.ref }}
|
||||||
|
# Ensures production-build.yml will always be fresh
|
||||||
|
key-4: ${{ github.sha }}
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/dist/**
|
||||||
|
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||||
|
- run: |
|
||||||
|
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||||
|
if [ ${{ steps.cache-atoms-build.outputs.cache-hit }} == 'true' ]; then
|
||||||
|
echo "Cache hit for atoms build. Skipping build."
|
||||||
|
else
|
||||||
|
rm -rf packages/platform/atoms/node_modules
|
||||||
|
yarn install
|
||||||
|
yarn workspace @calcom/atoms run build
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
@@ -14,7 +14,15 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Label PR as ready for E2E
|
- name: Label PR as ready for E2E
|
||||||
if: github.event.review.state == 'approved'
|
if: github.event.review.state == 'approved'
|
||||||
uses: actions-ecosystem/action-add-labels@v1
|
uses: actions/github-script@v4
|
||||||
with:
|
with:
|
||||||
|
script: |
|
||||||
|
await github.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
labels: process.env.labels.split(', '),
|
||||||
|
})
|
||||||
|
env:
|
||||||
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
|
github_token: ${{ secrets.READY_FOR_E2E_PAT }}
|
||||||
labels: 'ready-for-e2e'
|
labels: 'ready-for-e2e'
|
||||||
@@ -131,6 +131,13 @@ jobs:
|
|||||||
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
uses: ./.github/workflows/api-v2-production-build.yml
|
uses: ./.github/workflows/api-v2-production-build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
build-atoms:
|
||||||
|
name: Production builds
|
||||||
|
needs: [changes, check-label, deps]
|
||||||
|
if: ${{ needs.check-label.outputs.run-e2e == 'true' && needs.changes.outputs.has-files-requiring-all-checks == 'true' }}
|
||||||
|
uses: ./.github/workflows/atoms-production-build.yml
|
||||||
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Production builds
|
name: Production builds
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
"start": "nest start",
|
"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: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-utils build && yarn workspace @calcom/platform-types build",
|
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-enums build",
|
||||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/platform-constants": "*",
|
"@calcom/platform-constants": "*",
|
||||||
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.28",
|
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.28",
|
||||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
import { CreateEventTypeInput_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/create-event-type.input";
|
||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -9,7 +10,11 @@ import type { PrismaClient } from "@calcom/prisma";
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesRepository_2024_04_15 {
|
export class EventTypesRepository_2024_04_15 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(
|
||||||
|
private readonly dbRead: PrismaReadService,
|
||||||
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private usersService: UsersService
|
||||||
|
) {}
|
||||||
|
|
||||||
async createUserEventType(
|
async createUserEventType(
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -46,7 +51,7 @@ export class EventTypesRepository_2024_04_15 {
|
|||||||
eventTypeId: number
|
eventTypeId: number
|
||||||
) {
|
) {
|
||||||
return await getEventTypeById({
|
return await getEventTypeById({
|
||||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
currentOrganizationId: this.usersService.getUserMainOrgId(user),
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||||
|
|||||||
+7
-5
@@ -6,6 +6,7 @@ import { EventTypeOutput } from "@/ee/event-types/event-types_2024_04_15/outputs
|
|||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
import { UserWithProfile, UsersRepository } from "@/modules/users/users.repository";
|
||||||
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, ForbiddenException, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -24,7 +25,8 @@ export class EventTypesService_2024_04_15 {
|
|||||||
private readonly membershipsRepository: MembershipsRepository,
|
private readonly membershipsRepository: MembershipsRepository,
|
||||||
private readonly usersRepository: UsersRepository,
|
private readonly usersRepository: UsersRepository,
|
||||||
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
private readonly selectedCalendarsRepository: SelectedCalendarsRepository,
|
||||||
private readonly dbWrite: PrismaWriteService
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createUserEventType(
|
async createUserEventType(
|
||||||
@@ -53,11 +55,11 @@ export class EventTypesService_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToCreateEvent(user: UserWithProfile) {
|
async getUserToCreateEvent(user: UserWithProfile) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
const isOrgAdmin = organizationId
|
const isOrgAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
: false;
|
: false;
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
@@ -80,7 +82,7 @@ export class EventTypesService_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
const isUserOrganizationAdmin = organizationId
|
const isUserOrganizationAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
@@ -153,7 +155,7 @@ export class EventTypesService_2024_04_15 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -20,13 +20,13 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_06_14 } from "@calcom/platform-constants";
|
||||||
|
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
|
||||||
import {
|
import {
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateEventTypeInput_2024_06_14,
|
CreateEventTypeInput_2024_06_14,
|
||||||
EventTypeOutput_2024_06_14,
|
EventTypeOutput_2024_06_14,
|
||||||
UpdateEventTypeInput_2024_06_14,
|
UpdateEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/dist/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
|
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
describe("Event types Endpoints", () => {
|
describe("Event types Endpoints", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -34,7 +35,11 @@ type InputEventTransformed = Omit<
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EventTypesRepository_2024_06_14 {
|
export class EventTypesRepository_2024_06_14 {
|
||||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
constructor(
|
||||||
|
private readonly dbRead: PrismaReadService,
|
||||||
|
private readonly dbWrite: PrismaWriteService,
|
||||||
|
private usersService: UsersService
|
||||||
|
) {}
|
||||||
|
|
||||||
async createUserEventType(userId: number, body: InputEventTransformed) {
|
async createUserEventType(userId: number, body: InputEventTransformed) {
|
||||||
return this.dbWrite.prisma.eventType.create({
|
return this.dbWrite.prisma.eventType.create({
|
||||||
@@ -80,7 +85,7 @@ export class EventTypesRepository_2024_06_14 {
|
|||||||
eventTypeId: number
|
eventTypeId: number
|
||||||
) {
|
) {
|
||||||
return await getEventTypeById({
|
return await getEventTypeById({
|
||||||
currentOrganizationId: user.movedToProfile?.organizationId || user.organizationId,
|
currentOrganizationId: this.usersService.getUserMainOrgId(user),
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
prisma: this.dbRead.prisma as unknown as PrismaClient,
|
||||||
|
|||||||
+4
-4
@@ -119,11 +119,11 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToCreateEvent(user: UserWithProfile) {
|
async getUserToCreateEvent(user: UserWithProfile) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
const isOrgAdmin = organizationId
|
const isOrgAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
: false;
|
: false;
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -159,7 +159,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
async getUserEventTypeForAtom(user: UserWithProfile, eventTypeId: number) {
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
const isUserOrganizationAdmin = organizationId
|
const isUserOrganizationAdmin = organizationId
|
||||||
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
? await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId)
|
||||||
@@ -270,7 +270,7 @@ export class EventTypesService_2024_06_14 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserToUpdateEvent(user: UserWithProfile) {
|
async getUserToUpdateEvent(user: UserWithProfile) {
|
||||||
const profileId = user.movedToProfile?.id || null;
|
const profileId = this.usersService.getUserMainProfile(user)?.id || null;
|
||||||
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
const selectedCalendars = await this.selectedCalendarsRepository.getUserSelectedCalendars(user.id);
|
||||||
return { ...user, profile: { id: profileId }, selectedCalendars };
|
return { ...user, profile: { id: profileId }, selectedCalendars };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator";
|
||||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||||
import { Reflector } from "@nestjs/core";
|
import { Reflector } from "@nestjs/core";
|
||||||
@@ -12,13 +13,14 @@ export class OrganizationRolesGuard implements CanActivate {
|
|||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private organizationsService: OrganizationsService,
|
private organizationsService: OrganizationsService,
|
||||||
private membershipRepository: MembershipsRepository
|
private membershipRepository: MembershipsRepository,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const user: UserWithProfile = request.user;
|
const user: UserWithProfile = request.user;
|
||||||
const organizationId = user?.movedToProfile?.organizationId || user?.organizationId;
|
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||||
|
|
||||||
if (!user || !organizationId) {
|
if (!user || !organizationId) {
|
||||||
throw new ForbiddenException("No organization associated with the user.");
|
throw new ForbiddenException("No organization associated with the user.");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
|||||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||||
|
import { UsersModule } from "@/modules/users/users.module";
|
||||||
import { BullModule } from "@nestjs/bull";
|
import { BullModule } from "@nestjs/bull";
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import { Module } from "@nestjs/common";
|
|||||||
duration: 1000,
|
duration: 1000,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
UsersModule,
|
||||||
],
|
],
|
||||||
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
|
providers: [BillingConfigService, BillingService, BillingRepository, BillingProcessor],
|
||||||
exports: [BillingService, BillingRepository],
|
exports: [BillingService, BillingRepository],
|
||||||
|
|||||||
+5
-3
@@ -13,6 +13,7 @@ import { OAuthClientGuard } from "@/modules/oauth-clients/guards/oauth-client-gu
|
|||||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||||
import { UsersRepository } from "@/modules/users/users.repository";
|
import { UsersRepository } from "@/modules/users/users.repository";
|
||||||
import {
|
import {
|
||||||
@@ -59,7 +60,8 @@ export class OAuthClientsController {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly oauthClientRepository: OAuthClientRepository,
|
private readonly oauthClientRepository: OAuthClientRepository,
|
||||||
private readonly userRepository: UsersRepository,
|
private readonly userRepository: UsersRepository,
|
||||||
private readonly teamsRepository: OrganizationsRepository
|
private readonly teamsRepository: OrganizationsRepository,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post("/")
|
@Post("/")
|
||||||
@@ -74,7 +76,7 @@ export class OAuthClientsController {
|
|||||||
@GetUser() user: UserWithProfile,
|
@GetUser() user: UserWithProfile,
|
||||||
@Body() body: CreateOAuthClientInput
|
@Body() body: CreateOAuthClientInput
|
||||||
): Promise<CreateOAuthClientResponseDto> {
|
): Promise<CreateOAuthClientResponseDto> {
|
||||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||||
);
|
);
|
||||||
@@ -100,7 +102,7 @@ export class OAuthClientsController {
|
|||||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||||
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
async getOAuthClients(@GetUser() user: UserWithProfile): Promise<GetOAuthClientsResponseDto> {
|
||||||
const organizationId = (user.movedToProfile?.organizationId ?? user.organizationId) as number;
|
const organizationId = this.usersService.getUserMainOrgId(user) as number;
|
||||||
|
|
||||||
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
const clients = await this.oauthClientRepository.getOrganizationOAuthClients(organizationId);
|
||||||
return { status: SUCCESS_STATUS, data: clients };
|
return { status: SUCCESS_STATUS, data: clients };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
CanActivate,
|
CanActivate,
|
||||||
@@ -11,12 +12,12 @@ import { Request } from "express";
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OAuthClientGuard implements CanActivate {
|
export class OAuthClientGuard implements CanActivate {
|
||||||
constructor(private oAuthClientRepository: OAuthClientRepository) {}
|
constructor(private oAuthClientRepository: OAuthClientRepository, private usersService: UsersService) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
|
const request = context.switchToHttp().getRequest<Request & { user: GetUserReturnType }>();
|
||||||
const user: GetUserReturnType = request.user;
|
const user: GetUserReturnType = request.user;
|
||||||
const organizationId = user?.movedToProfile?.organizationId || user?.organizationId;
|
const organizationId = user ? this.usersService.getUserMainOrgId(user) : null;
|
||||||
const oAuthClientId = request.params.clientId;
|
const oAuthClientId = request.params.clientId;
|
||||||
|
|
||||||
if (!oAuthClientId) {
|
if (!oAuthClientId) {
|
||||||
|
|||||||
+1
-1
@@ -17,6 +17,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
|||||||
import { withApiAuth } from "test/utils/withApiAuth";
|
import { withApiAuth } from "test/utils/withApiAuth";
|
||||||
|
|
||||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||||
|
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-enums";
|
||||||
import {
|
import {
|
||||||
ApiSuccessResponse,
|
ApiSuccessResponse,
|
||||||
CreateTeamEventTypeInput_2024_06_14,
|
CreateTeamEventTypeInput_2024_06_14,
|
||||||
@@ -24,7 +25,6 @@ import {
|
|||||||
TeamEventTypeOutput_2024_06_14,
|
TeamEventTypeOutput_2024_06_14,
|
||||||
UpdateTeamEventTypeInput_2024_06_14,
|
UpdateTeamEventTypeInput_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import { BookingWindowPeriodInputTypeEnum_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
|
|
||||||
import { Team } from "@calcom/prisma/client";
|
import { Team } from "@calcom/prisma/client";
|
||||||
|
|
||||||
describe("Organizations Event Types Endpoints", () => {
|
describe("Organizations Event Types Endpoints", () => {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
CreateTeamEventTypeInput_2024_06_14,
|
CreateTeamEventTypeInput_2024_06_14,
|
||||||
UpdateTeamEventTypeInput_2024_06_14,
|
UpdateTeamEventTypeInput_2024_06_14,
|
||||||
HostPriority,
|
HostPriority,
|
||||||
SchedulingType,
|
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
|
import { SchedulingType } from "@calcom/prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InputOrganizationsEventTypesService {
|
export class InputOrganizationsEventTypesService {
|
||||||
|
|||||||
+2
-1
@@ -96,7 +96,8 @@ export class OrganizationsEventTypesService {
|
|||||||
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
||||||
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId);
|
const isOrgAdmin = await this.membershipsRepository.isUserOrganizationAdmin(user.id, organizationId);
|
||||||
const profileId =
|
const profileId =
|
||||||
this.usersService.getUserProfileByOrgId(user, organizationId)?.id || user.movedToProfileId;
|
this.usersService.getUserProfileByOrgId(user, organizationId)?.id ||
|
||||||
|
this.usersService.getUserMainProfile(user)?.id;
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
role: user.role,
|
role: user.role,
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ export class UsersService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserMainOrgId(user: UserWithProfile) {
|
||||||
|
return this.getUserMainProfile(user)?.organizationId ?? user.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
|
getUserProfileByOrgId(user: UserWithProfile, organizationId: number) {
|
||||||
return user.profiles?.find((p) => p.organizationId === organizationId);
|
return user.profiles?.find((p) => p.organizationId === organizationId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
import { GetUserReturnType } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||||
|
import { UsersService } from "@/modules/users/services/users.service";
|
||||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||||
import {
|
import {
|
||||||
CanActivate,
|
CanActivate,
|
||||||
@@ -17,7 +18,8 @@ import { PlatformOAuthClient, Webhook } from "@calcom/prisma/client";
|
|||||||
export class IsOAuthClientWebhookGuard implements CanActivate {
|
export class IsOAuthClientWebhookGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly webhooksService: WebhooksService,
|
private readonly webhooksService: WebhooksService,
|
||||||
private readonly oAuthClientRepository: OAuthClientRepository
|
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||||
|
private usersService: UsersService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
@@ -27,7 +29,7 @@ export class IsOAuthClientWebhookGuard implements CanActivate {
|
|||||||
const user = request.user as GetUserReturnType;
|
const user = request.user as GetUserReturnType;
|
||||||
const webhookId = request.params.webhookId;
|
const webhookId = request.params.webhookId;
|
||||||
const oAuthClientId = request.params.clientId;
|
const oAuthClientId = request.params.clientId;
|
||||||
const organizationId = user.movedToProfile?.organizationId || user.organizationId;
|
const organizationId = this.usersService.getUserMainOrgId(user);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new ForbiddenException("User not authenticated");
|
throw new ForbiddenException("User not authenticated");
|
||||||
|
|||||||
@@ -4955,6 +4955,9 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Source"
|
"$ref": "#/components/schemas/Source"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"disableOnPrefill": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -5529,6 +5532,9 @@
|
|||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/Source"
|
"$ref": "#/components/schemas/Source"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"disableOnPrefill": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import { _generateMetadata } from "app/_utils";
|
|
||||||
|
|
||||||
import Page from "@calcom/features/ee/sso/page/teams-sso-view";
|
|
||||||
|
|
||||||
export const generateMetadata = async () =>
|
|
||||||
await _generateMetadata(
|
|
||||||
(t) => t("sso_configuration"),
|
|
||||||
(t) => t("sso_configuration_description")
|
|
||||||
);
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
@@ -8,8 +8,24 @@ export default function NoPlatformPlan() {
|
|||||||
description="You are not subscribed to a Platform plan."
|
description="You are not subscribed to a Platform plan."
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button href="https://cal.com/platform/pricing">Go to Pricing</Button>
|
<Button
|
||||||
<Button color="secondary" href="https://cal.com/pricing">
|
className="hover:bg-slate-300 hover:text-black"
|
||||||
|
color="secondary"
|
||||||
|
href="/settings/platform/new">
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="hover:bg-slate-300 hover:text-black"
|
||||||
|
href="https://cal.com/platform/pricing"
|
||||||
|
target="_blank">
|
||||||
|
Go to Pricing
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="hover:bg-slate-300 hover:text-black"
|
||||||
|
href="https://cal.com/sales"
|
||||||
|
target="_blank">
|
||||||
Contact Sales
|
Contact Sales
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ export const platformPlans: IndividualPlatformPlan[] = [
|
|||||||
pricing: 99,
|
pricing: 99,
|
||||||
includes: [
|
includes: [
|
||||||
"Up to 100 bookings a month",
|
"Up to 100 bookings a month",
|
||||||
|
"$0.99 overage beyond",
|
||||||
"Community Support",
|
"Community Support",
|
||||||
"Cal Atoms (React Library)",
|
"Cal Atoms (React Library)",
|
||||||
"Platform APIs",
|
"Platform APIs",
|
||||||
"Admin APIs",
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -26,9 +26,9 @@ export const platformPlans: IndividualPlatformPlan[] = [
|
|||||||
"Your essential package with sophisticated support, hosted platform APIs, Cal.com Atoms (React components) and more.",
|
"Your essential package with sophisticated support, hosted platform APIs, Cal.com Atoms (React components) and more.",
|
||||||
pricing: 299,
|
pricing: 299,
|
||||||
includes: [
|
includes: [
|
||||||
"Up to 500 bookings a month. $0,60 overage beyond",
|
|
||||||
"Everything in Starter",
|
"Everything in Starter",
|
||||||
"Cal Atoms (React Library)",
|
"Up to 500 bookings a month",
|
||||||
|
"$0.60 overage beyond",
|
||||||
"User Management and Analytics",
|
"User Management and Analytics",
|
||||||
"Technical Account Manager and Onboarding Support",
|
"Technical Account Manager and Onboarding Support",
|
||||||
],
|
],
|
||||||
@@ -39,8 +39,9 @@ export const platformPlans: IndividualPlatformPlan[] = [
|
|||||||
"The best all-in-one plan to scale your company. Everything you need to provide scheduling for the masses, without breaking things.",
|
"The best all-in-one plan to scale your company. Everything you need to provide scheduling for the masses, without breaking things.",
|
||||||
pricing: 2499,
|
pricing: 2499,
|
||||||
includes: [
|
includes: [
|
||||||
"Up to 5000 bookings a month. $0.50 overage beyond",
|
|
||||||
"Everything in Essentials",
|
"Everything in Essentials",
|
||||||
|
"Up to 5000 bookings a month",
|
||||||
|
"$0.50 overage beyond",
|
||||||
"Credential import from other platforms",
|
"Credential import from other platforms",
|
||||||
"Compliance Check SOC2, HIPAA",
|
"Compliance Check SOC2, HIPAA",
|
||||||
"One-on-one developer calls",
|
"One-on-one developer calls",
|
||||||
@@ -51,7 +52,18 @@ export const platformPlans: IndividualPlatformPlan[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
plan: "Enterprise",
|
plan: "Enterprise",
|
||||||
description: "Everything in Scale with generous volume discounts beyond 50,000 bookings a month.",
|
description:
|
||||||
includes: ["Beyond 50,000 bookings a month", "Everything in Scale", "Up to 50% discount on overages"],
|
"Enterprise is perfect for institutions that focus on control, security and everything beyond.",
|
||||||
|
includes: [
|
||||||
|
"Everything in Scale",
|
||||||
|
"No overages",
|
||||||
|
"Credential import from other platforms",
|
||||||
|
"Compliance Check SOC2, HIPAA",
|
||||||
|
"One-on-one developer calls",
|
||||||
|
"Help with Credentials Verification (Zoom, Google App Store)",
|
||||||
|
"Expedited features and integrations",
|
||||||
|
"SLA (99.999% uptime)",
|
||||||
|
"Volume Discount",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -280,7 +280,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
|
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
|
||||||
<div className="flex-1 px-2">
|
<div className="flex-1 px-2">
|
||||||
<p className="text-subtle">{t("current_username")}</p>
|
<p className="text-subtle">{t("current_username")}</p>
|
||||||
<p className="text-emphasis mt-1" data-testid="current-username">
|
<p className="text-emphasis mt-1 break-all" data-testid="current-username">
|
||||||
{currentUsername}
|
{currentUsername}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -288,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
<p className="text-subtle" data-testid="new-username">
|
<p className="text-subtle" data-testid="new-username">
|
||||||
{t("new_username")}
|
{t("new_username")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-emphasis">{inputUsernameValue}</p>
|
<p className="text-emphasis break-all">{inputUsernameValue}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||||||
<p className="text-subtle">{t("current_username")}</p>
|
<p className="text-subtle">{t("current_username")}</p>
|
||||||
<Tooltip content={currentUsername}>
|
<Tooltip content={currentUsername}>
|
||||||
<p
|
<p
|
||||||
className="text-emphasis mt-1 max-w-md overflow-hidden text-ellipsis"
|
className="text-emphasis mt-1 max-w-md overflow-hidden text-ellipsis break-all"
|
||||||
data-testid="current-username">
|
data-testid="current-username">
|
||||||
{currentUsername}
|
{currentUsername}
|
||||||
</p>
|
</p>
|
||||||
@@ -175,7 +175,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||||||
{t("new_username")}
|
{t("new_username")}
|
||||||
</p>
|
</p>
|
||||||
<Tooltip content={inputUsernameValue}>
|
<Tooltip content={inputUsernameValue}>
|
||||||
<p className="text-emphasis mt-1 max-w-md overflow-hidden text-ellipsis">
|
<p className="text-emphasis mt-1 max-w-md overflow-hidden text-ellipsis break-all">
|
||||||
{inputUsernameValue}
|
{inputUsernameValue}
|
||||||
</p>
|
</p>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -741,7 +741,7 @@ export default function Success(props: PageProps) {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
{userIsOwner && !isCancelled && isRejectionMode && (
|
{!isCancelled && isRejectionMode && (
|
||||||
<>
|
<>
|
||||||
<hr className="border-subtle" />
|
<hr className="border-subtle" />
|
||||||
<RejectBooking
|
<RejectBooking
|
||||||
|
|||||||
@@ -170,7 +170,8 @@ const InfiniteMobileTeamsTab: FC<InfiniteMobileTeamsTabProps> = (props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
refetchOnWindowFocus: true,
|
refetchOnWindowFocus: true,
|
||||||
staleTime: 1 * 60 * 60 * 1000,
|
refetchOnMount: true,
|
||||||
|
staleTime: 0,
|
||||||
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
getNextPageParam: (lastPage) => lastPage.nextCursor,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "4.4.2",
|
"version": "4.4.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
"@calcom/embed-snippet": "workspace:*",
|
"@calcom/embed-snippet": "workspace:*",
|
||||||
"@calcom/features": "*",
|
"@calcom/features": "*",
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
|
"@calcom/platform-enums": "*",
|
||||||
"@calcom/platform-types": "*",
|
"@calcom/platform-types": "*",
|
||||||
"@calcom/prisma": "*",
|
"@calcom/prisma": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
|
|||||||
@@ -321,15 +321,6 @@ export default function Signup({
|
|||||||
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
|
||||||
{/* Left side */}
|
{/* Left side */}
|
||||||
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-12 2xl:px-28">
|
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-12 2xl:px-28">
|
||||||
{/* Header */}
|
|
||||||
{errors.apiError && (
|
|
||||||
<Alert
|
|
||||||
className="mb-3"
|
|
||||||
severity="error"
|
|
||||||
message={errors.apiError?.message}
|
|
||||||
data-testid="signup-error-message"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h1 className="font-cal text-[28px] leading-none ">
|
<h1 className="font-cal text-[28px] leading-none ">
|
||||||
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
|
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
|
||||||
@@ -408,6 +399,14 @@ export default function Signup({
|
|||||||
onChange={() => handleConsentChange(COOKIE_CONSENT)}
|
onChange={() => handleConsentChange(COOKIE_CONSENT)}
|
||||||
description={t("cookie_consent_checkbox")}
|
description={t("cookie_consent_checkbox")}
|
||||||
/>
|
/>
|
||||||
|
{errors.apiError && (
|
||||||
|
<Alert
|
||||||
|
className="mb-3"
|
||||||
|
severity="error"
|
||||||
|
message={errors.apiError?.message}
|
||||||
|
data-testid="signup-error-message"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="my-2 w-full justify-center"
|
className="my-2 w-full justify-center"
|
||||||
|
|||||||
@@ -7,15 +7,10 @@ import { MembershipRole } from "@calcom/prisma/enums";
|
|||||||
import { moveUserToOrg } from "@lib/orgMigration";
|
import { moveUserToOrg } from "@lib/orgMigration";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
|
import { IS_STRIPE_ENABLED } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
const IS_STRIPE_ENABLED = !!(
|
|
||||||
process.env.STRIPE_CLIENT_ID &&
|
|
||||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
|
||||||
process.env.STRIPE_PRIVATE_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
const IS_SELF_HOSTED = !(
|
const IS_SELF_HOSTED = !(
|
||||||
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") || !!new URL(WEBAPP_URL).hostname.endsWith(".cal.com")
|
new URL(WEBAPP_URL).hostname.endsWith(".cal.dev") || !!new URL(WEBAPP_URL).hostname.endsWith(".cal.com")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ export const createRoutingFormsFixture = () => {
|
|||||||
label: string;
|
label: string;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
options?: {
|
||||||
|
label: string;
|
||||||
|
id: string | null;
|
||||||
|
}[];
|
||||||
}[];
|
}[];
|
||||||
}) {
|
}) {
|
||||||
return await prisma.app_RoutingForms_Form.create({
|
return await prisma.app_RoutingForms_Form.create({
|
||||||
|
|||||||
@@ -301,6 +301,10 @@ export const createUsersFixture = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (scenario.seedRoutingForms) {
|
if (scenario.seedRoutingForms) {
|
||||||
|
const option2Uuid = "d1302635-9f12-17b1-9153-c3a854649182";
|
||||||
|
const option1Uuid = "d1292635-9f12-17b1-9153-c3a854649182";
|
||||||
|
const multiSelectLegacyFieldUuid = "d4292635-9f12-17b1-9153-c3a854649182";
|
||||||
|
const multiSelectFieldUuid = "d9892635-9f12-17b1-9153-c3a854649182";
|
||||||
await prisma.app_RoutingForms_Form.create({
|
await prisma.app_RoutingForms_Form.create({
|
||||||
data: {
|
data: {
|
||||||
routes: [
|
routes: [
|
||||||
@@ -366,6 +370,26 @@ export const createUsersFixture = (
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
||||||
|
action: { type: "customPageMessage", value: "Multiselect(Legacy) chosen" },
|
||||||
|
queryValue: {
|
||||||
|
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
||||||
|
type: "group",
|
||||||
|
children1: {
|
||||||
|
"b98a8abb-cdef-4012-b456-718262343d27": {
|
||||||
|
type: "rule",
|
||||||
|
properties: {
|
||||||
|
field: multiSelectLegacyFieldUuid,
|
||||||
|
value: [["Option-2"]],
|
||||||
|
operator: "multiselect_equals",
|
||||||
|
valueSrc: ["value"],
|
||||||
|
valueType: ["multiselect"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "bb9ea8b9-0123-4456-b89a-b182623406d8",
|
||||||
action: { type: "customPageMessage", value: "Multiselect chosen" },
|
action: { type: "customPageMessage", value: "Multiselect chosen" },
|
||||||
queryValue: {
|
queryValue: {
|
||||||
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
id: "aa8ba8b9-0123-4456-b89a-b182623406d8",
|
||||||
@@ -374,8 +398,8 @@ export const createUsersFixture = (
|
|||||||
"b98a8abb-cdef-4012-b456-718262343d27": {
|
"b98a8abb-cdef-4012-b456-718262343d27": {
|
||||||
type: "rule",
|
type: "rule",
|
||||||
properties: {
|
properties: {
|
||||||
field: "d4292635-9f12-17b1-9153-c3a854649182",
|
field: multiSelectFieldUuid,
|
||||||
value: [["Option-2"]],
|
value: [[option2Uuid]],
|
||||||
operator: "multiselect_equals",
|
operator: "multiselect_equals",
|
||||||
valueSrc: ["value"],
|
valueSrc: ["value"],
|
||||||
valueType: ["multiselect"],
|
valueType: ["multiselect"],
|
||||||
@@ -399,13 +423,30 @@ export const createUsersFixture = (
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "d4292635-9f12-17b1-9153-c3a854649182",
|
id: multiSelectLegacyFieldUuid,
|
||||||
type: "multiselect",
|
type: "multiselect",
|
||||||
label: "Multi Select",
|
label: "Multi Select(with Legacy `selectText`)",
|
||||||
identifier: "multi",
|
identifier: "multi",
|
||||||
selectText: "Option-1\nOption-2",
|
selectText: "Option-1\nOption-2",
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: multiSelectFieldUuid,
|
||||||
|
type: "multiselect",
|
||||||
|
label: "Multi Select",
|
||||||
|
identifier: "multi-new-format",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
id: option1Uuid,
|
||||||
|
label: "Option-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: option2Uuid,
|
||||||
|
label: "Option-2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
user: {
|
user: {
|
||||||
connect: {
|
connect: {
|
||||||
|
|||||||
@@ -7,19 +7,11 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|||||||
|
|
||||||
import { test, todo } from "./lib/fixtures";
|
import { test, todo } from "./lib/fixtures";
|
||||||
import type { Fixtures } from "./lib/fixtures";
|
import type { Fixtures } from "./lib/fixtures";
|
||||||
import { selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
import { IS_STRIPE_ENABLED, selectFirstAvailableTimeSlotNextMonth } from "./lib/testUtils";
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
const IS_STRIPE_ENABLED = !!(
|
|
||||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
|
||||||
process.env.STRIPE_CLIENT_ID &&
|
|
||||||
process.env.STRIPE_PRIVATE_KEY &&
|
|
||||||
process.env.PAYMENT_FEE_FIXED &&
|
|
||||||
process.env.PAYMENT_FEE_PERCENTAGE
|
|
||||||
);
|
|
||||||
|
|
||||||
test.describe("Stripe integration skip true", () => {
|
test.describe("Stripe integration skip true", () => {
|
||||||
// eslint-disable-next-line playwright/no-skipped-test
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
|
test.skip(!IS_STRIPE_ENABLED, "It should only run if Stripe is installed");
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ export const testName = "Test Testson";
|
|||||||
export const teamEventTitle = "Team Event - 30min";
|
export const teamEventTitle = "Team Event - 30min";
|
||||||
export const teamEventSlug = "team-event-30min";
|
export const teamEventSlug = "team-event-30min";
|
||||||
|
|
||||||
|
export const IS_STRIPE_ENABLED = !!(
|
||||||
|
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
||||||
|
process.env.STRIPE_CLIENT_ID &&
|
||||||
|
process.env.STRIPE_PRIVATE_KEY &&
|
||||||
|
process.env.PAYMENT_FEE_FIXED &&
|
||||||
|
process.env.PAYMENT_FEE_PERCENTAGE
|
||||||
|
);
|
||||||
|
|
||||||
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
|
export function createHttpServer(opts: { requestHandler?: RequestHandler } = {}) {
|
||||||
const {
|
const {
|
||||||
requestHandler = ({ res }) => {
|
requestHandler = ({ res }) => {
|
||||||
|
|||||||
@@ -12,14 +12,9 @@ import {
|
|||||||
bookTimeSlot,
|
bookTimeSlot,
|
||||||
doOnOrgDomain,
|
doOnOrgDomain,
|
||||||
goToUrlWithErrorHandling,
|
goToUrlWithErrorHandling,
|
||||||
|
IS_STRIPE_ENABLED,
|
||||||
} from "./lib/testUtils";
|
} from "./lib/testUtils";
|
||||||
|
|
||||||
const IS_STRIPE_ENABLED = !!(
|
|
||||||
process.env.STRIPE_CLIENT_ID &&
|
|
||||||
process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY &&
|
|
||||||
process.env.STRIPE_PRIVATE_KEY
|
|
||||||
);
|
|
||||||
|
|
||||||
test.describe.configure({ mode: "parallel" });
|
test.describe.configure({ mode: "parallel" });
|
||||||
|
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
@@ -164,6 +159,9 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
||||||
|
// eslint-disable-next-line playwright/no-skipped-test
|
||||||
|
test.skip(!IS_STRIPE_ENABLED, "Skipped as Stripe is not installed");
|
||||||
|
|
||||||
const user = await users.create();
|
const user = await users.create();
|
||||||
await user.apiLogin();
|
await user.apiLogin();
|
||||||
await user.installStripePersonal({ skip: true });
|
await user.installStripePersonal({ skip: true });
|
||||||
|
|||||||
@@ -647,6 +647,7 @@ test.describe("FORM_SUBMITTED", async () => {
|
|||||||
responses: {
|
responses: {
|
||||||
name: {
|
name: {
|
||||||
value: "John Doe",
|
value: "John Doe",
|
||||||
|
response: "John Doe",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -673,14 +674,33 @@ test.describe("FORM_SUBMITTED", async () => {
|
|||||||
identifier: "name",
|
identifier: "name",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "multiselect",
|
||||||
|
label: "Multi Select",
|
||||||
|
identifier: "multi",
|
||||||
|
required: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: "Option-1",
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Option-2",
|
||||||
|
id: "2",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.waitForLoadState("networkidle");
|
await page.waitForLoadState("networkidle");
|
||||||
|
|
||||||
await gotoRoutingLink({ page, formId: form.id });
|
await gotoRoutingLink({ page, formId: form.id });
|
||||||
const fieldName = "name";
|
const textFieldIdentifier = "name";
|
||||||
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
|
const multiSelectFieldIdentifier = "multi";
|
||||||
|
await page.fill(`[data-testid="form-field-${textFieldIdentifier}"]`, "John Doe");
|
||||||
|
await page.click(`[data-testid="form-field-${multiSelectFieldIdentifier}"]`); // Open dropdown
|
||||||
|
await page.click("text=Option-2"); // Select option
|
||||||
page.click('button[type="submit"]');
|
page.click('button[type="submit"]');
|
||||||
|
|
||||||
await webhookReceiver.waitForRequestCount(1);
|
await webhookReceiver.waitForRequestCount(1);
|
||||||
@@ -699,10 +719,23 @@ test.describe("FORM_SUBMITTED", async () => {
|
|||||||
responses: {
|
responses: {
|
||||||
name: {
|
name: {
|
||||||
value: "John Doe",
|
value: "John Doe",
|
||||||
|
response: "John Doe",
|
||||||
|
},
|
||||||
|
multi: {
|
||||||
|
value: ["Option-2"],
|
||||||
|
response: [
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
label: "Option-2",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/* Legacy fields Start */
|
||||||
name: "John Doe",
|
name: "John Doe",
|
||||||
|
multi: ["Option-2"],
|
||||||
|
/* Legacy fields End */
|
||||||
});
|
});
|
||||||
|
|
||||||
webhookReceiver.close();
|
webhookReceiver.close();
|
||||||
|
|||||||
@@ -92,6 +92,18 @@
|
|||||||
<path d="M3 10h18" ></path>
|
<path d="M3 10h18" ></path>
|
||||||
<path d="m16 20 2 2 4-4" ></path>
|
<path d="m16 20 2 2 4-4" ></path>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol class="lucide lucide-calendar-days" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="calendar-days">
|
||||||
|
<path d="M8 2v4" ></path>
|
||||||
|
<path d="M16 2v4" ></path>
|
||||||
|
<rect width="18" height="18" x="3" y="4" rx="2" ></rect>
|
||||||
|
<path d="M3 10h18" ></path>
|
||||||
|
<path d="M8 14h.01" ></path>
|
||||||
|
<path d="M12 14h.01" ></path>
|
||||||
|
<path d="M16 14h.01" ></path>
|
||||||
|
<path d="M8 18h.01" ></path>
|
||||||
|
<path d="M12 18h.01" ></path>
|
||||||
|
<path d="M16 18h.01" ></path>
|
||||||
|
</symbol>
|
||||||
<symbol class="lucide lucide-calendar-heart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="calendar-heart">
|
<symbol class="lucide lucide-calendar-heart" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="calendar-heart">
|
||||||
<path d="M3 10h18V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7" ></path>
|
<path d="M3 10h18V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7" ></path>
|
||||||
<path d="M8 2v4" ></path>
|
<path d="M8 2v4" ></path>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 46 KiB |
@@ -81,15 +81,24 @@ declare global {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace jest {
|
namespace jest {
|
||||||
interface Matchers<R> {
|
interface Matchers<R> {
|
||||||
toHaveEmail(expectedEmail: ExpectedEmail, to: string): R;
|
toHaveEmail(expectedEmail: ExpectedEmail, to: string, subject?: string): R;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toHaveEmail(emails: Fixtures["emails"], expectedEmail: ExpectedEmail, to: string) {
|
toHaveEmail(emails: Fixtures["emails"], expectedEmail: ExpectedEmail, to: string, subject?: string) {
|
||||||
const { isNot } = this;
|
const { isNot } = this;
|
||||||
const testEmail = emails.get().find((email) => email.to.includes(to));
|
const testEmail = emails.get().find((email) => {
|
||||||
|
const filterConditions = [email.to.includes(to)];
|
||||||
|
|
||||||
|
if (subject) {
|
||||||
|
filterConditions.push(email.subject === subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterConditions.every((condition) => condition);
|
||||||
|
});
|
||||||
|
|
||||||
const emailsToLog = emails
|
const emailsToLog = emails
|
||||||
.get()
|
.get()
|
||||||
.map((email) => ({ to: email.to, html: email.html, ics: email.icalEvent }));
|
.map((email) => ({ to: email.to, html: email.html, ics: email.icalEvent }));
|
||||||
@@ -742,10 +751,12 @@ export function expectSuccessfulBookingRescheduledEmails({
|
|||||||
export function expectAwaitingPaymentEmails({
|
export function expectAwaitingPaymentEmails({
|
||||||
emails,
|
emails,
|
||||||
booker,
|
booker,
|
||||||
|
subject,
|
||||||
}: {
|
}: {
|
||||||
emails: Fixtures["emails"];
|
emails: Fixtures["emails"];
|
||||||
organizer: { email: string; name: string };
|
organizer: { email: string; name: string };
|
||||||
booker: { email: string; name: string };
|
booker: { email: string; name: string };
|
||||||
|
subject?: string;
|
||||||
}) {
|
}) {
|
||||||
expect(emails).toHaveEmail(
|
expect(emails).toHaveEmail(
|
||||||
{
|
{
|
||||||
@@ -753,7 +764,8 @@ export function expectAwaitingPaymentEmails({
|
|||||||
to: `${booker.name} <${booker.email}>`,
|
to: `${booker.name} <${booker.email}>`,
|
||||||
noIcs: true,
|
noIcs: true,
|
||||||
},
|
},
|
||||||
`${booker.email}`
|
`${booker.email}`,
|
||||||
|
subject
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,17 +277,16 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
dateTime: dayjs(event.endTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
|
dateTime: dayjs(event.endTime).tz(event.organizer.timeZone).format("YYYY-MM-DDTHH:mm:ss"),
|
||||||
timeZone: event.organizer.timeZone,
|
timeZone: event.organizer.timeZone,
|
||||||
},
|
},
|
||||||
attendees: [
|
organizer: {
|
||||||
// Add the calEvent organizer
|
emailAddress: {
|
||||||
{
|
address: event.destinationCalendar
|
||||||
emailAddress: {
|
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
|
||||||
address: event.destinationCalendar
|
event.organizer.email
|
||||||
? event.destinationCalendar.find((cal) => cal.userId === event.organizer.id)?.externalId ??
|
: event.organizer.email,
|
||||||
event.organizer.email
|
name: event.organizer.name,
|
||||||
: event.organizer.email,
|
|
||||||
name: event.organizer.name,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
attendees: [
|
||||||
...event.attendees.map((attendee) => ({
|
...event.attendees.map((attendee) => ({
|
||||||
emailAddress: {
|
emailAddress: {
|
||||||
address: attendee.email,
|
address: attendee.email,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermis
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { getSerializableForm } from "../../lib/getSerializableForm";
|
import { getSerializableForm } from "../../lib/getSerializableForm";
|
||||||
import type { Response, SerializableForm } from "../../types/types";
|
import type { FormResponse, SerializableForm } from "../../types/types";
|
||||||
|
|
||||||
function escapeCsvText(str: string) {
|
function escapeCsvText(str: string) {
|
||||||
return str.replace(/,/, "%2C");
|
return str.replace(/,/, "%2C");
|
||||||
@@ -32,7 +32,7 @@ async function* getResponses(
|
|||||||
) {
|
) {
|
||||||
const csv: string[] = [];
|
const csv: string[] = [];
|
||||||
responses.forEach((response) => {
|
responses.forEach((response) => {
|
||||||
const fieldResponses = response.response as Response;
|
const fieldResponses = response.response as FormResponse;
|
||||||
const csvCells: string[] = [];
|
const csvCells: string[] = [];
|
||||||
headerFields.forEach((headerField) => {
|
headerFields.forEach((headerField) => {
|
||||||
const fieldResponse = fieldResponses[headerField.id];
|
const fieldResponse = fieldResponses[headerField.id];
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import type { Dispatch, SetStateAction } from "react";
|
|||||||
import getFieldIdentifier from "../lib/getFieldIdentifier";
|
import getFieldIdentifier from "../lib/getFieldIdentifier";
|
||||||
import { getQueryBuilderConfig } from "../lib/getQueryBuilderConfig";
|
import { getQueryBuilderConfig } from "../lib/getQueryBuilderConfig";
|
||||||
import isRouterLinkedField from "../lib/isRouterLinkedField";
|
import isRouterLinkedField from "../lib/isRouterLinkedField";
|
||||||
import transformResponse from "../lib/transformResponse";
|
import { getUIOptionsForSelect } from "../lib/selectOptions";
|
||||||
import type { SerializableForm, Response } from "../types/types";
|
import { getFieldResponseForJsonLogic } from "../lib/transformResponse";
|
||||||
|
import type { SerializableForm, FormResponse } from "../types/types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
form: SerializableForm<App_RoutingForms_Form>;
|
form: SerializableForm<App_RoutingForms_Form>;
|
||||||
response: Response;
|
response: FormResponse;
|
||||||
setResponse: Dispatch<SetStateAction<Response>>;
|
setResponse: Dispatch<SetStateAction<FormResponse>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FormInputFields(props: Props) {
|
export default function FormInputFields(props: Props) {
|
||||||
@@ -31,14 +32,7 @@ export default function FormInputFields(props: Props) {
|
|||||||
}
|
}
|
||||||
const Component = widget.factory;
|
const Component = widget.factory;
|
||||||
|
|
||||||
const optionValues = field.selectText?.trim().split("\n");
|
const options = getUIOptionsForSelect(field);
|
||||||
const options = optionValues?.map((value) => {
|
|
||||||
const title = value;
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div key={field.id} className="mb-4 block flex-col sm:flex ">
|
<div key={field.id} className="mb-4 block flex-col sm:flex ">
|
||||||
<div className="min-w-48 mb-2 flex-grow">
|
<div className="min-w-48 mb-2 flex-grow">
|
||||||
@@ -62,7 +56,7 @@ export default function FormInputFields(props: Props) {
|
|||||||
...response,
|
...response,
|
||||||
[field.id]: {
|
[field.id]: {
|
||||||
label: field.label,
|
label: field.label,
|
||||||
value: transformResponse({ field, value }),
|
value: getFieldResponseForJsonLogic({ field, value }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ import { getAbsoluteEventTypeRedirectUrl } from "../getEventTypeRedirectUrl";
|
|||||||
import { RoutingPages } from "../lib/RoutingPages";
|
import { RoutingPages } from "../lib/RoutingPages";
|
||||||
import { isFallbackRoute } from "../lib/isFallbackRoute";
|
import { isFallbackRoute } from "../lib/isFallbackRoute";
|
||||||
import { processRoute } from "../lib/processRoute";
|
import { processRoute } from "../lib/processRoute";
|
||||||
import type { Response, Route, SerializableForm } from "../types/types";
|
import type { FormResponse, Route, SerializableForm } from "../types/types";
|
||||||
import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions";
|
import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions";
|
||||||
import FormInputFields from "./FormInputFields";
|
import FormInputFields from "./FormInputFields";
|
||||||
import RoutingNavBar from "./RoutingNavBar";
|
import RoutingNavBar from "./RoutingNavBar";
|
||||||
@@ -238,7 +238,7 @@ function SingleForm({ form, appUrl, Page, enrichedWithUserProfileForm }: SingleF
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false);
|
const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false);
|
||||||
const [response, setResponse] = useState<Response>({});
|
const [response, setResponse] = useState<FormResponse>({});
|
||||||
const [decidedAction, setDecidedAction] = useState<Route["action"] | null>(null);
|
const [decidedAction, setDecidedAction] = useState<Route["action"] | null>(null);
|
||||||
const [skipFirstUpdate, setSkipFirstUpdate] = useState(true);
|
const [skipFirstUpdate, setSkipFirstUpdate] = useState(true);
|
||||||
const [eventTypeUrl, setEventTypeUrl] = useState("");
|
const [eventTypeUrl, setEventTypeUrl] = useState("");
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FieldTypes } from "../pages/form-edit/[...appPages]";
|
import { FieldTypes } from "../pages/form-edit/[...appPages]";
|
||||||
import type { QueryBuilderUpdatedConfig, RoutingForm } from "../types/types";
|
import type { QueryBuilderUpdatedConfig, RoutingForm } from "../types/types";
|
||||||
import { InitialConfig } from "./InitialConfig";
|
import { InitialConfig } from "./InitialConfig";
|
||||||
|
import { getUIOptionsForSelect } from "./selectOptions";
|
||||||
|
|
||||||
export function getQueryBuilderConfig(form: RoutingForm, forReporting = false) {
|
export function getQueryBuilderConfig(form: RoutingForm, forReporting = false) {
|
||||||
const fields: Record<
|
const fields: Record<
|
||||||
@@ -22,14 +23,7 @@ export function getQueryBuilderConfig(form: RoutingForm, forReporting = false) {
|
|||||||
field = field.routerField;
|
field = field.routerField;
|
||||||
}
|
}
|
||||||
if (FieldTypes.map((f) => f.value).includes(field.type)) {
|
if (FieldTypes.map((f) => f.value).includes(field.type)) {
|
||||||
const optionValues = field.selectText?.trim().split("\n");
|
const options = getUIOptionsForSelect(field);
|
||||||
const options = optionValues?.map((value) => {
|
|
||||||
const title = value;
|
|
||||||
return {
|
|
||||||
value,
|
|
||||||
title,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const widget = InitialConfig.widgets[field.type];
|
const widget = InitialConfig.widgets[field.type];
|
||||||
const widgetType = widget.type;
|
const widgetType = widget.type;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { zodFields, zodRoutes } from "../zod";
|
|||||||
import getConnectedForms from "./getConnectedForms";
|
import getConnectedForms from "./getConnectedForms";
|
||||||
import isRouter from "./isRouter";
|
import isRouter from "./isRouter";
|
||||||
import isRouterLinkedField from "./isRouterLinkedField";
|
import isRouterLinkedField from "./isRouterLinkedField";
|
||||||
|
import { getFieldWithOptions } from "./selectOptions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Doesn't have deleted fields by default
|
* Doesn't have deleted fields by default
|
||||||
@@ -57,7 +58,9 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
|
|||||||
name: f.name,
|
name: f.name,
|
||||||
description: f.description,
|
description: f.description,
|
||||||
}));
|
}));
|
||||||
const finalFields = fields;
|
|
||||||
|
const finalFields = fields.map((field) => getFieldWithOptions(field));
|
||||||
|
|
||||||
let teamMembers: SerializableFormTeamMembers[] = [];
|
let teamMembers: SerializableFormTeamMembers[] = [];
|
||||||
if (form.teamId) {
|
if (form.teamId) {
|
||||||
teamMembers = await prisma.user.findMany({
|
teamMembers = await prisma.user.findMany({
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { App_RoutingForms_Form } from "@prisma/client";
|
|||||||
import { Utils as QbUtils } from "react-awesome-query-builder";
|
import { Utils as QbUtils } from "react-awesome-query-builder";
|
||||||
import type { z } from "zod";
|
import type { z } from "zod";
|
||||||
|
|
||||||
import type { Response, Route, SerializableForm } from "../types/types";
|
import type { FormResponse, Route, SerializableForm } from "../types/types";
|
||||||
import type { zodNonRouterRoute } from "../zod";
|
import type { zodNonRouterRoute } from "../zod";
|
||||||
import { getQueryBuilderConfig } from "./getQueryBuilderConfig";
|
import { getQueryBuilderConfig } from "./getQueryBuilderConfig";
|
||||||
import { isFallbackRoute } from "./isFallbackRoute";
|
import { isFallbackRoute } from "./isFallbackRoute";
|
||||||
@@ -16,7 +16,7 @@ export function processRoute({
|
|||||||
response,
|
response,
|
||||||
}: {
|
}: {
|
||||||
form: SerializableForm<App_RoutingForms_Form>;
|
form: SerializableForm<App_RoutingForms_Form>;
|
||||||
response: Record<string, Pick<Response[string], "value">>;
|
response: Record<string, Pick<FormResponse[string], "value">>;
|
||||||
}) {
|
}) {
|
||||||
const queryBuilderConfig = getQueryBuilderConfig(form);
|
const queryBuilderConfig = getQueryBuilderConfig(form);
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export function processRoute({
|
|||||||
const jsonLogicQuery = QbUtils.jsonLogicFormat(state.tree, state.config);
|
const jsonLogicQuery = QbUtils.jsonLogicFormat(state.tree, state.config);
|
||||||
const logic = jsonLogicQuery.logic;
|
const logic = jsonLogicQuery.logic;
|
||||||
let result = false;
|
let result = false;
|
||||||
const responseValues: Record<string, Response[string]["value"]> = {};
|
const responseValues: Record<string, FormResponse[string]["value"]> = {};
|
||||||
for (const [uuid, { value }] of Object.entries(response)) {
|
for (const [uuid, { value }] of Object.entries(response)) {
|
||||||
responseValues[uuid] = value;
|
responseValues[uuid] = value;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* @fileoverview
|
||||||
|
*
|
||||||
|
* This file holds the utilities to build the options to render in the select field and it could be loaded on client side as well.
|
||||||
|
*/
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import type { zodFieldView } from "../zod";
|
||||||
|
|
||||||
|
type Field = z.infer<typeof zodFieldView>;
|
||||||
|
|
||||||
|
const buildOptionsFromLegacySelectText = ({ legacySelectText }: { legacySelectText: string }) => {
|
||||||
|
return legacySelectText
|
||||||
|
.trim()
|
||||||
|
.split("\n")
|
||||||
|
.map((fieldValue) => ({
|
||||||
|
label: fieldValue,
|
||||||
|
id: null,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFieldWithOptions = <T extends Field>(field: T) => {
|
||||||
|
const legacySelectText = field.selectText;
|
||||||
|
if (field.options) {
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
options: field.options,
|
||||||
|
};
|
||||||
|
} else if (legacySelectText) {
|
||||||
|
const options = buildOptionsFromLegacySelectText({ legacySelectText });
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getUIOptionsForSelect(field: Field) {
|
||||||
|
return getFieldWithOptions(field).options?.map((option) => {
|
||||||
|
// We prefer option.id as that doesn't change when we change the option text/label.
|
||||||
|
// Fallback to option.label for fields saved in DB in old format which didn't have `options`
|
||||||
|
const value = option.id ?? option.label;
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
title: option.label,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
|
|
||||||
import type { Response, Route, Field } from "../types/types";
|
import type { FormResponse, Route, Field } from "../types/types";
|
||||||
import getFieldIdentifier from "./getFieldIdentifier";
|
import getFieldIdentifier from "./getFieldIdentifier";
|
||||||
|
|
||||||
export const substituteVariables = (
|
export const substituteVariables = (
|
||||||
routeValue: Route["action"]["value"],
|
routeValue: Route["action"]["value"],
|
||||||
response: Response,
|
response: FormResponse,
|
||||||
fields: Field[]
|
fields: Field[]
|
||||||
) => {
|
) => {
|
||||||
const regex = /\{([^\}]+)\}/g;
|
const regex = /\{([^\}]+)\}/g;
|
||||||
@@ -15,7 +15,11 @@ export const substituteVariables = (
|
|||||||
|
|
||||||
variables.forEach((variable) => {
|
variables.forEach((variable) => {
|
||||||
for (const key in response) {
|
for (const key in response) {
|
||||||
const identifier = getFieldIdentifier(fields.find((field) => field.id === key));
|
const field = fields.find((field) => field.id === key)
|
||||||
|
if (!field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const identifier = getFieldIdentifier(field);
|
||||||
if (identifier.toLowerCase() === variable.toLowerCase()) {
|
if (identifier.toLowerCase() === variable.toLowerCase()) {
|
||||||
eventTypeUrl = eventTypeUrl.replace(`{${variable}}`, slugify(response[key].value.toString() || ""));
|
eventTypeUrl = eventTypeUrl.replace(`{${variable}}`, slugify(response[key].value.toString() || ""));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { getFieldResponseForJsonLogic } from "./transformResponse";
|
||||||
|
|
||||||
|
describe("getFieldResponseForJsonLogic", () => {
|
||||||
|
it("should return an empty string if value is undefined", () => {
|
||||||
|
const field = { type: "text", options: undefined };
|
||||||
|
const value = undefined;
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should transform value for a number type field to number", () => {
|
||||||
|
const field = { type: "number", options: undefined };
|
||||||
|
const value = "123";
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toBe(123);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("multiselect", () => {
|
||||||
|
describe("non-legacy options", () => {
|
||||||
|
it("should return option ids for the field if the value is an array of option ids", () => {
|
||||||
|
const field = {
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: "1", label: "Option 1" },
|
||||||
|
{ id: "2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = ["1", "2"];
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toEqual(["1", "2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return ids if matching labels are provided in value", () => {
|
||||||
|
const field = {
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: "1", label: "Option 1" },
|
||||||
|
{ id: "2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = ["Option 1", "Option 2"];
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toEqual(["1", "2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the value as it is if it doesn't match any of the labels or option ids", () => {
|
||||||
|
const field = {
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: "1", label: "Option 1" },
|
||||||
|
{ id: "2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = ["Option 10", "Option 11"];
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toEqual(["Option 10", "Option 11"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("legacy options", () => {
|
||||||
|
it("should return what is given for a field that has legacy options but the values don't match labels", () => {
|
||||||
|
const field = {
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: null, label: "Option 1" },
|
||||||
|
{ id: null, label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = ["1", "2"];
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toEqual(["1", "2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return matching labels for a field that has legacy options", () => {
|
||||||
|
const field = {
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: null, label: "Option 1" },
|
||||||
|
{ id: null, label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = ["Option 1", "Option 2"];
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toEqual(["Option 1", "Option 2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiselect field with legacy options", () => {
|
||||||
|
const field = {
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ label: "Option 1", id: null },
|
||||||
|
{ label: "Option 2", id: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = ["Option 1", "Option 2"];
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toEqual(["Option 1", "Option 2"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("select", () => {
|
||||||
|
it("should handle select field with options", () => {
|
||||||
|
const field = {
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ id: "1", label: "Option 1" },
|
||||||
|
{ id: "2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = "1";
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toBe("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle select field with legacy options", () => {
|
||||||
|
const field = {
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Option 1", id: null },
|
||||||
|
{ label: "Option 2", id: null },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = "Option 1";
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toBe("Option 1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle select field with number as string", () => {
|
||||||
|
const field = {
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ id: "1", label: "Option 1" },
|
||||||
|
{ id: "2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const value = 1;
|
||||||
|
const result = getFieldResponseForJsonLogic({ field, value });
|
||||||
|
expect(result).toBe("1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,59 @@
|
|||||||
import type { Field, Response } from "../types/types";
|
import type { Field, FormResponse } from "../types/types";
|
||||||
|
|
||||||
export default function transformResponse({
|
/**
|
||||||
|
* It takes care of correctly transforming the input to label or id depending on various cases
|
||||||
|
* - It allows us to prefill with ID or Label
|
||||||
|
* - It allows backward compatibility with legacy routes(with labels for options)
|
||||||
|
*/
|
||||||
|
function transformSelectValue({
|
||||||
|
field,
|
||||||
|
idOrLabel,
|
||||||
|
}: {
|
||||||
|
field: Pick<Field, "options" | "type">;
|
||||||
|
idOrLabel: string;
|
||||||
|
}) {
|
||||||
|
idOrLabel = idOrLabel.trim();
|
||||||
|
const options = field.options;
|
||||||
|
if (!options) {
|
||||||
|
return idOrLabel;
|
||||||
|
}
|
||||||
|
const areOptionsInLegacyFormat = !!options.find((option) => !option.id);
|
||||||
|
// Because for legacy saved options, routes must have labels in them instead of ids
|
||||||
|
const shouldUseLabelAsValue = areOptionsInLegacyFormat;
|
||||||
|
const foundOptionById = options.find((option) => option.id === idOrLabel);
|
||||||
|
if (foundOptionById) {
|
||||||
|
if (shouldUseLabelAsValue) {
|
||||||
|
return foundOptionById.label;
|
||||||
|
} else {
|
||||||
|
// If shouldUseLabelAsValue is false, then we must use id as value
|
||||||
|
// Because shouldUseLabelAsValue is false, id must be set already
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return foundOptionById.id!;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No option was found that matches ID
|
||||||
|
// So check if the label is provided
|
||||||
|
const foundOptionByLabel = options.find((option) => {
|
||||||
|
return option.label === idOrLabel;
|
||||||
|
});
|
||||||
|
if (foundOptionByLabel) {
|
||||||
|
if (!shouldUseLabelAsValue) {
|
||||||
|
// If shouldUseLabelAsValue is false, then we must use id as value
|
||||||
|
// Because shouldUseLabelAsValue is false, id must be set already
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
return foundOptionByLabel.id!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return idOrLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFieldResponseForJsonLogic({
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
field: Field;
|
field: Pick<Field, "options" | "type">;
|
||||||
value: Response[string]["value"] | undefined;
|
value: FormResponse[string]["value"] | undefined;
|
||||||
}) {
|
}) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return "";
|
return "";
|
||||||
@@ -18,13 +66,24 @@ export default function transformResponse({
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
if (field.type === "multiselect") {
|
if (field.type === "multiselect") {
|
||||||
if (value instanceof Array) {
|
// Could be option id(i.e. a UUIDv4) or option label for ease of prefilling
|
||||||
return value;
|
let valueOrLabelArray = value instanceof Array ? value : value.toString().split(",");
|
||||||
}
|
|
||||||
return value
|
valueOrLabelArray = valueOrLabelArray.map((idOrLabel) => {
|
||||||
.toString()
|
return transformSelectValue({ field, idOrLabel });
|
||||||
.split(",")
|
});
|
||||||
.map((v) => v.trim());
|
|
||||||
|
return valueOrLabelArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === "select") {
|
||||||
|
const valueAsStringOrStringArray = typeof value === "number" ? String(value) : value;
|
||||||
|
const valueAsString =
|
||||||
|
valueAsStringOrStringArray instanceof Array
|
||||||
|
? valueAsStringOrStringArray[0]
|
||||||
|
: valueAsStringOrStringArray;
|
||||||
|
|
||||||
|
return transformSelectValue({ field, idOrLabel: valueAsString });
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useEffect, useState } from "react";
|
import type { ClipboardEvent } from "react";
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { Controller, useFieldArray, useWatch } from "react-hook-form";
|
import { Controller, useFieldArray, useWatch } from "react-hook-form";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
@@ -29,8 +29,26 @@ import SingleForm, {
|
|||||||
} from "../../components/SingleForm";
|
} from "../../components/SingleForm";
|
||||||
|
|
||||||
export { getServerSideProps };
|
export { getServerSideProps };
|
||||||
|
type SelectOption = { label: string; id: string | null };
|
||||||
type HookForm = UseFormReturn<RoutingFormWithResponseCount>;
|
type HookForm = UseFormReturn<RoutingFormWithResponseCount>;
|
||||||
type SelectOption = { placeholder: string; value: string; id: string };
|
|
||||||
|
const appendArray = <T,>({
|
||||||
|
target,
|
||||||
|
arrayToAppend,
|
||||||
|
appendAt,
|
||||||
|
}: {
|
||||||
|
arrayToAppend: T[];
|
||||||
|
target: T[];
|
||||||
|
appendAt: number;
|
||||||
|
}) => {
|
||||||
|
// Avoid mutating the original array
|
||||||
|
const copyOfTarget = [...target];
|
||||||
|
const numItemsToRemove = arrayToAppend.length;
|
||||||
|
copyOfTarget.splice(appendAt, numItemsToRemove, ...arrayToAppend);
|
||||||
|
return copyOfTarget;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PASTE_OPTIONS_SEPARATOR_REGEX = /\n+/;
|
||||||
|
|
||||||
export const FieldTypes = [
|
export const FieldTypes = [
|
||||||
{
|
{
|
||||||
@@ -90,65 +108,51 @@ function Field({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
|
const watchedOptions =
|
||||||
|
useWatch({
|
||||||
|
control: hookForm.control,
|
||||||
|
name: `${hookFieldNamespace}.options`,
|
||||||
|
defaultValue: [
|
||||||
|
{ label: "", id: uuidv4() },
|
||||||
|
{ label: "", id: uuidv4() },
|
||||||
|
{ label: "", id: uuidv4() },
|
||||||
|
{ label: "", id: uuidv4() },
|
||||||
|
],
|
||||||
|
}) || [];
|
||||||
|
|
||||||
const [options, setOptions] = useState<SelectOption[]>([
|
const setOptions = (updatedOptions: SelectOption[]) => {
|
||||||
{ placeholder: "< 10", value: "", id: uuidv4() },
|
hookForm.setValue(`${hookFieldNamespace}.options`, updatedOptions);
|
||||||
{ placeholder: "10-100", value: "", id: uuidv4() },
|
|
||||||
{ placeholder: "100-500", value: "", id: uuidv4() },
|
|
||||||
{ placeholder: "> 500", value: "", id: uuidv4() },
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleRemoveOptions = (index: number) => {
|
|
||||||
const updatedOptions = options.filter((_, i) => i !== index);
|
|
||||||
setOptions(updatedOptions);
|
|
||||||
updateSelectText(updatedOptions);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAddOptions = () => {
|
const handleRemoveOptions = (index: number) => {
|
||||||
setOptions((prevState) => [
|
const updatedOptions = watchedOptions.filter((_, i) => i !== index);
|
||||||
...prevState,
|
// We can't let the user remove the last option
|
||||||
|
if (updatedOptions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOptions(updatedOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addOption = () => {
|
||||||
|
setOptions([
|
||||||
|
...watchedOptions,
|
||||||
{
|
{
|
||||||
placeholder: "New Option",
|
label: "",
|
||||||
value: "",
|
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const originalValues = hookForm.getValues(`${hookFieldNamespace}.selectText`);
|
|
||||||
if (originalValues) {
|
|
||||||
const values: SelectOption[] = originalValues.split("\n").map((fieldValue) => ({
|
|
||||||
value: fieldValue,
|
|
||||||
placeholder: "",
|
|
||||||
id: uuidv4(),
|
|
||||||
}));
|
|
||||||
setOptions(values);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const router = hookForm.getValues(`${hookFieldNamespace}.router`);
|
const router = hookForm.getValues(`${hookFieldNamespace}.router`);
|
||||||
const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`);
|
const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`);
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>, optionIndex: number) => {
|
const updateLabelAtIndex = ({ label, optionIndex }: { label: string; optionIndex: number }) => {
|
||||||
const updatedOptions = options.map((opt, index) => ({
|
const updatedOptions = watchedOptions.map((opt, index) => ({
|
||||||
...opt,
|
...opt,
|
||||||
...(index === optionIndex ? { value: e.target.value } : {}),
|
...(index === optionIndex ? { label } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setOptions(updatedOptions);
|
setOptions(updatedOptions);
|
||||||
updateSelectText(updatedOptions);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateSelectText = (updatedOptions: SelectOption[]) => {
|
|
||||||
hookForm.setValue(
|
|
||||||
`${hookFieldNamespace}.selectText`,
|
|
||||||
updatedOptions
|
|
||||||
.filter((opt) => opt.value)
|
|
||||||
.map((opt) => opt.value)
|
|
||||||
.join("\n")
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const label = useWatch({
|
const label = useWatch({
|
||||||
@@ -162,18 +166,49 @@ function Field({
|
|||||||
});
|
});
|
||||||
|
|
||||||
function move(index: number, increment: 1 | -1) {
|
function move(index: number, increment: 1 | -1) {
|
||||||
const newList = [...options];
|
const newList = [...watchedOptions];
|
||||||
|
|
||||||
const type = options[index];
|
const type = watchedOptions[index];
|
||||||
const tmp = options[index + increment];
|
const tmp = watchedOptions[index + increment];
|
||||||
if (tmp) {
|
if (tmp) {
|
||||||
newList[index] = tmp;
|
newList[index] = tmp;
|
||||||
newList[index + increment] = type;
|
newList[index + increment] = type;
|
||||||
}
|
}
|
||||||
setOptions(newList);
|
setOptions(newList);
|
||||||
updateSelectText(newList);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePasteInOptionAtIndex = ({
|
||||||
|
event,
|
||||||
|
optionIndex,
|
||||||
|
}: {
|
||||||
|
event: ClipboardEvent;
|
||||||
|
optionIndex: number;
|
||||||
|
}) => {
|
||||||
|
const paste = event.clipboardData.getData("text");
|
||||||
|
// The value being pasted could be a list of options
|
||||||
|
const optionsBeingPasted = paste
|
||||||
|
.split(PASTE_OPTIONS_SEPARATOR_REGEX)
|
||||||
|
.map((optionLabel) => optionLabel.trim())
|
||||||
|
.filter((optionLabel) => optionLabel)
|
||||||
|
.map((optionLabel) => ({ label: optionLabel.trim(), id: uuidv4() }));
|
||||||
|
if (optionsBeingPasted.length === 1) {
|
||||||
|
// If there is only one option, we would just let that option be pasted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow pasting that value, as we would update the options through state update
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const updatedOptions = appendArray({
|
||||||
|
target: watchedOptions,
|
||||||
|
arrayToAppend: optionsBeingPasted,
|
||||||
|
appendAt: optionIndex,
|
||||||
|
});
|
||||||
|
setOptions(updatedOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionsPlaceholders = ["< 10", "10 - 100", "100 - 500", "> 500"];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="field"
|
data-testid="field"
|
||||||
@@ -261,10 +296,15 @@ function Field({
|
|||||||
{t("options")}
|
{t("options")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<ul ref={animationRef}>
|
<ul ref={animationRef}>
|
||||||
{options.map((field, index) => (
|
{watchedOptions.map((option, index) => (
|
||||||
<li key={`select-option-${field.id}`} className="group mt-2 flex items-center gap-2">
|
<li
|
||||||
|
key={`select-option-${option.id}`}
|
||||||
|
className="group mt-2 flex items-center gap-2"
|
||||||
|
onPaste={(event: ClipboardEvent) =>
|
||||||
|
handlePasteInOptionAtIndex({ event, optionIndex: index })
|
||||||
|
}>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{options.length && index !== 0 ? (
|
{watchedOptions.length && index !== 0 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => move(index, -1)}
|
onClick={() => move(index, -1)}
|
||||||
@@ -272,7 +312,7 @@ function Field({
|
|||||||
<Icon name="arrow-up" />
|
<Icon name="arrow-up" />
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
{options.length && index !== options.length - 1 ? (
|
{watchedOptions.length && index !== watchedOptions.length - 1 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => move(index, 1)}
|
onClick={() => move(index, 1)}
|
||||||
@@ -287,18 +327,22 @@ function Field({
|
|||||||
containerClassName="[&>*:first-child]:border [&>*:first-child]:border-default hover:[&>*:first-child]:border-gray-400"
|
containerClassName="[&>*:first-child]:border [&>*:first-child]:border-default hover:[&>*:first-child]:border-gray-400"
|
||||||
className="border-0 focus:ring-0 focus:ring-offset-0"
|
className="border-0 focus:ring-0 focus:ring-offset-0"
|
||||||
labelSrOnly
|
labelSrOnly
|
||||||
placeholder={field.placeholder.toString()}
|
placeholder={optionsPlaceholders[index] ?? "New Option"}
|
||||||
value={field.value}
|
value={option.label}
|
||||||
type="text"
|
type="text"
|
||||||
|
required
|
||||||
addOnClassname="bg-transparent border-0"
|
addOnClassname="bg-transparent border-0"
|
||||||
onChange={(e) => handleChange(e, index)}
|
onChange={(e) => updateLabelAtIndex({ label: e.target.value, optionIndex: index })}
|
||||||
|
dataTestid={`${hookFieldNamespace}.options.${index}`}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<button
|
watchedOptions.length > 1 ? (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => handleRemoveOptions(index)}
|
type="button"
|
||||||
aria-label={t("remove")}>
|
onClick={() => handleRemoveOptions(index)}
|
||||||
<Icon name="x" className="h-4 w-4" />
|
aria-label={t("remove")}>
|
||||||
</button>
|
<Icon name="x" className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
) : null
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +356,7 @@ function Field({
|
|||||||
type="button"
|
type="button"
|
||||||
StartIcon="plus"
|
StartIcon="plus"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={handleAddOptions}>
|
onClick={addOption}>
|
||||||
Add an option
|
Add an option
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
|||||||
import { getSerializableForm } from "../../lib/getSerializableForm";
|
import { getSerializableForm } from "../../lib/getSerializableForm";
|
||||||
import { processRoute } from "../../lib/processRoute";
|
import { processRoute } from "../../lib/processRoute";
|
||||||
import { substituteVariables } from "../../lib/substituteVariables";
|
import { substituteVariables } from "../../lib/substituteVariables";
|
||||||
import transformResponse from "../../lib/transformResponse";
|
import { getFieldResponseForJsonLogic } from "../../lib/transformResponse";
|
||||||
import type { Response } from "../../types/types";
|
import type { FormResponse } from "../../types/types";
|
||||||
import { isAuthorizedToViewTheForm } from "../routing-link/getServerSideProps";
|
import { isAuthorizedToViewTheForm } from "../routing-link/getServerSideProps";
|
||||||
|
|
||||||
const log = logger.getSubLogger({ prefix: ["[routing-forms]", "[router]"] });
|
const log = logger.getSubLogger({ prefix: ["[routing-forms]", "[router]"] });
|
||||||
@@ -96,7 +96,7 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
form: enrichFormWithMigrationData(formWithUserProfile),
|
form: enrichFormWithMigrationData(formWithUserProfile),
|
||||||
});
|
});
|
||||||
|
|
||||||
const response: Response = {};
|
const response: FormResponse = {};
|
||||||
if (!serializableForm.fields) {
|
if (!serializableForm.fields) {
|
||||||
throw new Error("Form has no fields");
|
throw new Error("Form has no fields");
|
||||||
}
|
}
|
||||||
@@ -105,7 +105,7 @@ export const getServerSideProps = async function getServerSideProps(
|
|||||||
|
|
||||||
response[field.id] = {
|
response[field.id] = {
|
||||||
label: field.label,
|
label: field.label,
|
||||||
value: transformResponse({ field, value: fieldResponse }),
|
value: getFieldResponseForJsonLogic({ field, value: fieldResponse }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,10 @@ import { getAbsoluteEventTypeRedirectUrl } from "../../getEventTypeRedirectUrl";
|
|||||||
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||||
import { processRoute } from "../../lib/processRoute";
|
import { processRoute } from "../../lib/processRoute";
|
||||||
import { substituteVariables } from "../../lib/substituteVariables";
|
import { substituteVariables } from "../../lib/substituteVariables";
|
||||||
import transformResponse from "../../lib/transformResponse";
|
import { getFieldResponseForJsonLogic } from "../../lib/transformResponse";
|
||||||
import type { NonRouterRoute, Response } from "../../types/types";
|
import type { NonRouterRoute, FormResponse } from "../../types/types";
|
||||||
import { getServerSideProps } from "./getServerSideProps";
|
import { getServerSideProps } from "./getServerSideProps";
|
||||||
|
import { getUrlSearchParamsToForward } from "./getUrlSearchParamsToForward";
|
||||||
|
|
||||||
type Props = inferSSRProps<typeof getServerSideProps>;
|
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||||
const useBrandColors = ({
|
const useBrandColors = ({
|
||||||
@@ -59,10 +60,13 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
|
|||||||
// - like a network error
|
// - like a network error
|
||||||
// - or he abandoned booking flow in between
|
// - or he abandoned booking flow in between
|
||||||
const formFillerId = formFillerIdRef.current;
|
const formFillerId = formFillerIdRef.current;
|
||||||
const decidedActionWithFormResponseRef = useRef<{ action: NonRouterRoute["action"]; response: Response }>();
|
const decidedActionWithFormResponseRef = useRef<{
|
||||||
|
action: NonRouterRoute["action"];
|
||||||
|
response: FormResponse;
|
||||||
|
}>();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onSubmit = (response: Response) => {
|
const onSubmit = (response: FormResponse) => {
|
||||||
const decidedAction = processRoute({ form, response });
|
const decidedAction = processRoute({ form, response });
|
||||||
|
|
||||||
if (!decidedAction) {
|
if (!decidedAction) {
|
||||||
@@ -97,7 +101,11 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
|
|||||||
if (!fields) {
|
if (!fields) {
|
||||||
throw new Error("Routing Form fields must exist here");
|
throw new Error("Routing Form fields must exist here");
|
||||||
}
|
}
|
||||||
const allURLSearchParams = getUrlSearchParamsToForward(decidedActionWithFormResponse.response, fields);
|
const allURLSearchParams = getUrlSearchParamsToForward({
|
||||||
|
formResponse: decidedActionWithFormResponse.response,
|
||||||
|
fields,
|
||||||
|
searchParams: new URLSearchParams(window.location.search),
|
||||||
|
});
|
||||||
const decidedAction = decidedActionWithFormResponse.action;
|
const decidedAction = decidedActionWithFormResponse.action;
|
||||||
sdkActionManager?.fire("routed", {
|
sdkActionManager?.fire("routed", {
|
||||||
actionType: decidedAction.type,
|
actionType: decidedAction.type,
|
||||||
@@ -191,55 +199,6 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUrlSearchParamsToForward(response: Response, fields: NonNullable<Props["form"]["fields"]>) {
|
|
||||||
type Params = Record<string, string | string[]>;
|
|
||||||
const paramsFromResponse: Params = {};
|
|
||||||
const paramsFromCurrentUrl: Params = {};
|
|
||||||
|
|
||||||
// Build query params from response
|
|
||||||
Object.entries(response).forEach(([key, fieldResponse]) => {
|
|
||||||
const foundField = fields.find((f) => f.id === key);
|
|
||||||
if (!foundField) {
|
|
||||||
// If for some reason, the field isn't there, let's just
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const valueAsStringOrStringArray =
|
|
||||||
typeof fieldResponse.value === "number" ? String(fieldResponse.value) : fieldResponse.value;
|
|
||||||
paramsFromResponse[getFieldIdentifier(foundField) as keyof typeof paramsFromResponse] =
|
|
||||||
valueAsStringOrStringArray;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build query params from current URL. It excludes route params
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
for (const [name, value] of new URLSearchParams(window.location.search).entries()) {
|
|
||||||
const target = paramsFromCurrentUrl[name];
|
|
||||||
if (target instanceof Array) {
|
|
||||||
target.push(value);
|
|
||||||
} else {
|
|
||||||
paramsFromCurrentUrl[name] = [value];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const allQueryParams: Params = {
|
|
||||||
...paramsFromCurrentUrl,
|
|
||||||
// In case of conflict b/w paramsFromResponse and paramsFromCurrentUrl, paramsFromResponse should win as the booker probably improved upon the prefilled value.
|
|
||||||
...paramsFromResponse,
|
|
||||||
};
|
|
||||||
|
|
||||||
const allQueryURLSearchParams = new URLSearchParams();
|
|
||||||
|
|
||||||
// Make serializable URLSearchParams instance
|
|
||||||
Object.entries(allQueryParams).forEach(([param, value]) => {
|
|
||||||
const valueArray = value instanceof Array ? value : [value];
|
|
||||||
valueArray.forEach((v) => {
|
|
||||||
allQueryURLSearchParams.append(param, v);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return allQueryURLSearchParams;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RoutingLink(props: inferSSRProps<typeof getServerSideProps>) {
|
export default function RoutingLink(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
return <RoutingForm {...props} />;
|
return <RoutingForm {...props} />;
|
||||||
}
|
}
|
||||||
@@ -250,7 +209,7 @@ export { getServerSideProps };
|
|||||||
|
|
||||||
const usePrefilledResponse = (form: Props["form"]) => {
|
const usePrefilledResponse = (form: Props["form"]) => {
|
||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const prefillResponse: Response = {};
|
const prefillResponse: FormResponse = {};
|
||||||
|
|
||||||
// Prefill the form from query params
|
// Prefill the form from query params
|
||||||
form.fields?.forEach((field) => {
|
form.fields?.forEach((field) => {
|
||||||
@@ -259,10 +218,10 @@ const usePrefilledResponse = (form: Props["form"]) => {
|
|||||||
const value = valuesFromQuery.length > 1 ? valuesFromQuery : valuesFromQuery[0];
|
const value = valuesFromQuery.length > 1 ? valuesFromQuery : valuesFromQuery[0];
|
||||||
|
|
||||||
prefillResponse[field.id] = {
|
prefillResponse[field.id] = {
|
||||||
value: transformResponse({ field, value }),
|
value: getFieldResponseForJsonLogic({ field, value }),
|
||||||
label: field.label,
|
label: field.label,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const [response, setResponse] = useState<Response>(prefillResponse);
|
const [response, setResponse] = useState<FormResponse>(prefillResponse);
|
||||||
return [response, setResponse] as const;
|
return [response, setResponse] as const;
|
||||||
};
|
};
|
||||||
+216
@@ -0,0 +1,216 @@
|
|||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { getUrlSearchParamsToForward } from "./getUrlSearchParamsToForward";
|
||||||
|
|
||||||
|
function fromEntriesWithDuplicateKeys(entries: IterableIterator<[string, string]> | null) {
|
||||||
|
const result: Record<string, string | string[]> = {};
|
||||||
|
|
||||||
|
if (entries === null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider setting atleast ES2015 as target
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
for (const [key, value] of entries) {
|
||||||
|
if (result.hasOwnProperty(key)) {
|
||||||
|
let currentValue = result[key];
|
||||||
|
if (!Array.isArray(currentValue)) {
|
||||||
|
currentValue = [currentValue];
|
||||||
|
}
|
||||||
|
currentValue.push(value);
|
||||||
|
result[key] = currentValue;
|
||||||
|
} else {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getUrlSearchParamsToForward", () => {
|
||||||
|
it("should build query params from response correctly when identifier is present in fields", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const formResponse = {
|
||||||
|
[field1Id]: { value: "value1", label: "Field 1" },
|
||||||
|
[field2Id]: { value: ["option1", "option2"], label: "Field 2" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ id: field1Id, identifier: "f1", type: "text", label: "Field 1" },
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
identifier: "f2",
|
||||||
|
label: "Field 2",
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: "option1", label: "Option 1" },
|
||||||
|
{ id: "option2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams("?query1=value1&query2=value2");
|
||||||
|
const expectedParams = {
|
||||||
|
f1: "value1",
|
||||||
|
f2: ["Option 1", "Option 2"],
|
||||||
|
query1: "value1",
|
||||||
|
query2: "value2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getUrlSearchParamsToForward({
|
||||||
|
formResponse,
|
||||||
|
fields,
|
||||||
|
searchParams,
|
||||||
|
});
|
||||||
|
expect(fromEntriesWithDuplicateKeys(result.entries())).toEqual(expectedParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should build query params from response correctly when identifier is not present in fields. Should fallback to label", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const formResponse = {
|
||||||
|
[field1Id]: { value: "value1" },
|
||||||
|
[field2Id]: { value: "value2" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ id: field1Id, label: "Field 1", type: "text" },
|
||||||
|
{ id: field2Id, label: "Field 2", type: "text" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams("?query1=value1&query2=value2");
|
||||||
|
|
||||||
|
const expectedParams = {
|
||||||
|
"Field 1": "value1",
|
||||||
|
"Field 2": "value2",
|
||||||
|
query1: "value1",
|
||||||
|
query2: "value2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getUrlSearchParamsToForward({
|
||||||
|
formResponse,
|
||||||
|
fields,
|
||||||
|
searchParams,
|
||||||
|
});
|
||||||
|
expect(fromEntriesWithDuplicateKeys(result.entries())).toEqual(expectedParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle select fields correctly when options have id set", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const formResponse = {
|
||||||
|
[field1Id]: { value: "Option 1" },
|
||||||
|
[field2Id]: { value: ["Option 1", "Option 2"] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
id: field1Id,
|
||||||
|
label: "Field 1",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ id: "option1", label: "Option 1" },
|
||||||
|
{ id: "option2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: "option1", label: "Option 1" },
|
||||||
|
{ id: "option2", label: "Option 2" },
|
||||||
|
{ id: "option3", label: "Option 3" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams("?query1=value1&query2=value2");
|
||||||
|
|
||||||
|
const expectedParams = {
|
||||||
|
"Field 1": "Option 1",
|
||||||
|
"Field 2": ["Option 1", "Option 2"],
|
||||||
|
query1: "value1",
|
||||||
|
query2: "value2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getUrlSearchParamsToForward({
|
||||||
|
formResponse,
|
||||||
|
fields,
|
||||||
|
searchParams,
|
||||||
|
});
|
||||||
|
expect(fromEntriesWithDuplicateKeys(result.entries())).toEqual(expectedParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle select fields correctly when options have no id set(Legacy options)", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const formResponse = {
|
||||||
|
[field1Id]: { value: "Option 1" },
|
||||||
|
[field2Id]: { value: ["Option 1", "Option 2"] },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
id: field1Id,
|
||||||
|
label: "Field 1",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ id: null, label: "Option 1" },
|
||||||
|
{ id: null, label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
type: "multiselect",
|
||||||
|
options: [
|
||||||
|
{ id: "option1", label: "Option 1" },
|
||||||
|
{ id: "option2", label: "Option 2" },
|
||||||
|
{ id: "option3", label: "Option 3" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams("?query1=value1&query2=value2");
|
||||||
|
|
||||||
|
const expectedParams = {
|
||||||
|
"Field 1": "Option 1",
|
||||||
|
"Field 2": ["Option 1", "Option 2"],
|
||||||
|
query1: "value1",
|
||||||
|
query2: "value2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getUrlSearchParamsToForward({
|
||||||
|
formResponse,
|
||||||
|
fields,
|
||||||
|
searchParams,
|
||||||
|
});
|
||||||
|
expect(fromEntriesWithDuplicateKeys(result.entries())).toEqual(expectedParams);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle number values correctly", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const response = {
|
||||||
|
[field1Id]: { value: 123 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const fields = [{ id: field1Id, label: "Field 1", type: "number" }];
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams("?query1=value1&query2=value2");
|
||||||
|
const expectedParams = {
|
||||||
|
"Field 1": "123",
|
||||||
|
query1: "value1",
|
||||||
|
query2: "value2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getUrlSearchParamsToForward({
|
||||||
|
formResponse: response,
|
||||||
|
fields,
|
||||||
|
searchParams,
|
||||||
|
});
|
||||||
|
expect(fromEntriesWithDuplicateKeys(result.entries())).toEqual(expectedParams);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
|
||||||
|
import getFieldIdentifier from "../../lib/getFieldIdentifier";
|
||||||
|
import type { getServerSideProps } from "./getServerSideProps";
|
||||||
|
|
||||||
|
type Props = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
export function getUrlSearchParamsToForward({
|
||||||
|
formResponse,
|
||||||
|
fields,
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
formResponse: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
value: number | string | string[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
fields: Pick<
|
||||||
|
NonNullable<Props["form"]["fields"]>[number],
|
||||||
|
"id" | "type" | "options" | "identifier" | "label"
|
||||||
|
>[];
|
||||||
|
searchParams: URLSearchParams;
|
||||||
|
}) {
|
||||||
|
type Params = Record<string, string | string[]>;
|
||||||
|
const paramsFromResponse: Params = {};
|
||||||
|
const paramsFromCurrentUrl: Params = {};
|
||||||
|
|
||||||
|
// Build query params from response
|
||||||
|
Object.entries(formResponse).forEach(([key, fieldResponse]) => {
|
||||||
|
const foundField = fields.find((f) => f.id === key);
|
||||||
|
if (!foundField) {
|
||||||
|
// If for some reason, the field isn't there, let's just
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let valueAsStringOrStringArray =
|
||||||
|
typeof fieldResponse.value === "number" ? String(fieldResponse.value) : fieldResponse.value;
|
||||||
|
if (foundField.type === "select" || foundField.type === "multiselect") {
|
||||||
|
const options = foundField.options || [];
|
||||||
|
let arr =
|
||||||
|
valueAsStringOrStringArray instanceof Array
|
||||||
|
? valueAsStringOrStringArray
|
||||||
|
: [valueAsStringOrStringArray];
|
||||||
|
arr = arr.map((idOrLabel) => {
|
||||||
|
const foundOptionById = options.find((option) => {
|
||||||
|
return option.id === idOrLabel;
|
||||||
|
});
|
||||||
|
if (foundOptionById) {
|
||||||
|
return foundOptionById.label;
|
||||||
|
}
|
||||||
|
return idOrLabel;
|
||||||
|
});
|
||||||
|
valueAsStringOrStringArray = foundField.type === "select" ? arr[0] : arr;
|
||||||
|
}
|
||||||
|
paramsFromResponse[getFieldIdentifier(foundField) as keyof typeof paramsFromResponse] =
|
||||||
|
valueAsStringOrStringArray;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build query params from current URL. It excludes route params
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
for (const [name, value] of searchParams.entries()) {
|
||||||
|
const target = paramsFromCurrentUrl[name];
|
||||||
|
if (target instanceof Array) {
|
||||||
|
target.push(value);
|
||||||
|
} else {
|
||||||
|
paramsFromCurrentUrl[name] = [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const allQueryParams: Params = {
|
||||||
|
...paramsFromCurrentUrl,
|
||||||
|
// In case of conflict b/w paramsFromResponse and paramsFromCurrentUrl, paramsFromResponse should win as the booker probably improved upon the prefilled value.
|
||||||
|
...paramsFromResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
const allQueryURLSearchParams = new URLSearchParams();
|
||||||
|
|
||||||
|
// Make serializable URLSearchParams instance
|
||||||
|
Object.entries(allQueryParams).forEach(([param, value]) => {
|
||||||
|
const valueArray = value instanceof Array ? value : [value];
|
||||||
|
valueArray.forEach((v) => {
|
||||||
|
allQueryURLSearchParams.append(param, v);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return allQueryURLSearchParams;
|
||||||
|
}
|
||||||
@@ -17,6 +17,11 @@ function todo(title: string) {
|
|||||||
test.skip(title, () => {});
|
test.skip(title, () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Identifiers = {
|
||||||
|
multi: "multi",
|
||||||
|
multiNewFormat: "multi-new-format",
|
||||||
|
};
|
||||||
|
|
||||||
test.describe("Routing Forms", () => {
|
test.describe("Routing Forms", () => {
|
||||||
test.describe("Zero State Routing Forms", () => {
|
test.describe("Zero State Routing Forms", () => {
|
||||||
test("should be able to add a new form and view it", async ({ page }) => {
|
test("should be able to add a new form and view it", async ({ page }) => {
|
||||||
@@ -254,11 +259,11 @@ test.describe("Routing Forms", () => {
|
|||||||
responses.push(rowResponses);
|
responses.push(rowResponses);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(headers).toEqual(["Test field", "Multi Select"]);
|
expect(headers).toEqual(["Test field", "Multi Select(with Legacy `selectText`)", "Multi Select"]);
|
||||||
expect(responses).toEqual([
|
expect(responses).toEqual([
|
||||||
["event-routing", ""],
|
["event-routing", "", ""],
|
||||||
["external-redirect", ""],
|
["external-redirect", "", ""],
|
||||||
["custom-page", ""],
|
["custom-page", "", ""],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
||||||
@@ -281,19 +286,21 @@ test.describe("Routing Forms", () => {
|
|||||||
});
|
});
|
||||||
const csvRows = csv.trim().split("\n");
|
const csvRows = csv.trim().split("\n");
|
||||||
const csvHeaderRow = csvRows[0];
|
const csvHeaderRow = csvRows[0];
|
||||||
expect(csvHeaderRow).toEqual("Test field,Multi Select,Submission Time");
|
expect(csvHeaderRow).toEqual(
|
||||||
|
"Test field,Multi Select(with Legacy `selectText`),Multi Select,Submission Time"
|
||||||
|
);
|
||||||
|
|
||||||
const firstResponseCells = csvRows[1].split(",");
|
const firstResponseCells = csvRows[1].split(",");
|
||||||
const secondResponseCells = csvRows[2].split(",");
|
const secondResponseCells = csvRows[2].split(",");
|
||||||
const thirdResponseCells = csvRows[3].split(",");
|
const thirdResponseCells = csvRows[3].split(",");
|
||||||
|
|
||||||
expect(firstResponseCells.slice(0, -1).join(",")).toEqual("event-routing,");
|
expect(firstResponseCells.slice(0, -1).join(",")).toEqual("event-routing,,");
|
||||||
expect(new Date(firstResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
expect(new Date(firstResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
||||||
|
|
||||||
expect(secondResponseCells.slice(0, -1).join(",")).toEqual("external-redirect,");
|
expect(secondResponseCells.slice(0, -1).join(",")).toEqual("external-redirect,,");
|
||||||
expect(new Date(secondResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
expect(new Date(secondResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
||||||
|
|
||||||
expect(thirdResponseCells.slice(0, -1).join(",")).toEqual("custom-page,");
|
expect(thirdResponseCells.slice(0, -1).join(",")).toEqual("custom-page,,");
|
||||||
expect(new Date(thirdResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
expect(new Date(thirdResponseCells.at(-1) as string).getDay()).toEqual(new Date().getDay());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -318,8 +325,13 @@ test.describe("Routing Forms", () => {
|
|||||||
await page.goto(`/router?form=${routingForm.id}&Test field=custom-page`);
|
await page.goto(`/router?form=${routingForm.id}&Test field=custom-page`);
|
||||||
await expect(page.locator("text=Custom Page Result")).toBeVisible();
|
await expect(page.locator("text=Custom Page Result")).toBeVisible();
|
||||||
|
|
||||||
await page.goto(`/router?form=${routingForm.id}&Test field=doesntmatter&multi=Option-2`);
|
await page.goto(`/router?form=${routingForm.id}&Test field=doesntmatter&${Identifiers.multi}=Option-2`);
|
||||||
await expect(page.locator("text=Multiselect chosen")).toBeVisible();
|
await expect(page.locator("text=Multiselect(Legacy) chosen")).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
await page.goto(
|
||||||
|
`/router?form=${routingForm.id}&Test field=doesntmatter&${Identifiers.multiNewFormat}=d1302635-9f12-17b1-9153-c3a854649182`
|
||||||
|
);
|
||||||
|
await expect(page.locator("text=Multiselect chosen")).toBeVisible({ timeout: 10000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Routing Link should validate fields", async ({ page, users }) => {
|
test("Routing Link should validate fields", async ({ page, users }) => {
|
||||||
@@ -364,8 +376,34 @@ test.describe("Routing Forms", () => {
|
|||||||
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
||||||
expect(routingType).toBe("External Redirect");
|
expect(routingType).toBe("External Redirect");
|
||||||
expect(route).toBe("https://google.com");
|
expect(route).toBe("https://google.com");
|
||||||
|
await page.click('[data-testid="dialog-rejection"]');
|
||||||
|
|
||||||
|
// Multiselect(Legacy)
|
||||||
|
await page.click('[data-testid="test-preview"]');
|
||||||
|
await page.fill('[data-testid="form-field-Test field"]', "doesntmatter");
|
||||||
|
await page.click(`[data-testid="form-field-${Identifiers.multi}"]`); // Open dropdown
|
||||||
|
await page.click("text=Option-2"); // Select option
|
||||||
|
await page.click('[data-testid="test-routing"]');
|
||||||
|
routingType = await page.locator('[data-testid="test-routing-result-type"]').innerText();
|
||||||
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
||||||
|
expect(routingType).toBe("Custom Page");
|
||||||
|
expect(route).toBe("Multiselect(Legacy) chosen");
|
||||||
|
await page.click('[data-testid="dialog-rejection"]');
|
||||||
|
|
||||||
|
// Multiselect
|
||||||
|
await page.click('[data-testid="test-preview"]');
|
||||||
|
await page.fill('[data-testid="form-field-Test field"]', "doesntmatter");
|
||||||
|
await page.click(`[data-testid="form-field-${Identifiers.multiNewFormat}"]`); // Open dropdown
|
||||||
|
await page.click("text=Option-2"); // Select option
|
||||||
|
await page.click('[data-testid="test-routing"]');
|
||||||
|
routingType = await page.locator('[data-testid="test-routing-result-type"]').innerText();
|
||||||
|
route = await page.locator('[data-testid="test-routing-result"]').innerText();
|
||||||
|
expect(routingType).toBe("Custom Page");
|
||||||
|
expect(route).toBe("Multiselect chosen");
|
||||||
|
await page.click('[data-testid="dialog-rejection"]');
|
||||||
|
|
||||||
//fallback route
|
//fallback route
|
||||||
|
await page.click('[data-testid="test-preview"]');
|
||||||
await page.fill('[data-testid="form-field-Test field"]', "fallback");
|
await page.fill('[data-testid="form-field-Test field"]', "fallback");
|
||||||
await page.click('[data-testid="test-routing"]');
|
await page.click('[data-testid="test-routing"]');
|
||||||
routingType = await page.locator('[data-testid="test-routing-result-type"]').innerText();
|
routingType = await page.locator('[data-testid="test-routing-result-type"]').innerText();
|
||||||
@@ -452,8 +490,11 @@ async function addAllTypesOfFieldsAndSaveForm(
|
|||||||
identifier = "firstField";
|
identifier = "firstField";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fieldTypeLabel === "MultiSelect" || fieldTypeLabel === "Select") {
|
if (fieldTypeLabel === "Multiple Selection" || fieldTypeLabel === "Single Selection") {
|
||||||
await page.fill(`[name="fields.${nth}.selectText"]`, "123\n456\n789");
|
await page.fill(`[data-testid="fields.${nth}.options.0-input"]`, "123");
|
||||||
|
await page.fill(`[data-testid="fields.${nth}.options.1-input"]`, "456");
|
||||||
|
await page.fill(`[data-testid="fields.${nth}.options.2-input"]`, "789");
|
||||||
|
await page.fill(`[data-testid="fields.${nth}.options.3-input"]`, "10-11-12");
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.fill(`[name="fields.${nth}.label"]`, label);
|
await page.fill(`[name="fields.${nth}.label"]`, label);
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
|
||||||
|
import { buildResponsesForReporting } from "./report.handler";
|
||||||
|
|
||||||
|
describe("buildResponsesForReporting", () => {
|
||||||
|
it("for fields with options, it should return the labels of the options", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
|
||||||
|
const responsesFromDb = [
|
||||||
|
{
|
||||||
|
[field1Id]: { value: "value1" },
|
||||||
|
[field2Id]: { value: ["option1", "option2"] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ id: field1Id, label: "Field 1" },
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
options: [
|
||||||
|
{ id: "option1", label: "Option 1" },
|
||||||
|
{ id: "option2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedResponses = [["value1", "Option 1, Option 2"]];
|
||||||
|
|
||||||
|
const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields });
|
||||||
|
expect(responses).toEqual(expectedResponses);
|
||||||
|
expect(headers).toEqual(["Field 1", "Field 2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("for fields with options having null id, it should return what DB provided(as that would be labels only).", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const responsesFromDb = [
|
||||||
|
{
|
||||||
|
[field1Id]: { value: "value1" },
|
||||||
|
[field2Id]: { value: ["Option 1", "Option 2"] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ id: field1Id, label: "Field 1" },
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
options: [
|
||||||
|
{ id: null, label: "Option 1" },
|
||||||
|
{ id: null, label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedResponses = [["value1", "Option 1, Option 2"]];
|
||||||
|
|
||||||
|
const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields });
|
||||||
|
expect(responses).toEqual(expectedResponses);
|
||||||
|
expect(headers).toEqual(["Field 1", "Field 2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("for fields with no options but having array value, it should return the value as is", () => {
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const responsesFromDb = [
|
||||||
|
{
|
||||||
|
[field2Id]: { value: ["Option 1", "Option 2"] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedResponses = [["Option 1, Option 2"]];
|
||||||
|
|
||||||
|
const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields });
|
||||||
|
expect(responses).toEqual(expectedResponses);
|
||||||
|
expect(headers).toEqual(["Field 2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly handle numeric responses converting them to strings", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const responsesFromDb = [
|
||||||
|
{
|
||||||
|
[field1Id]: { value: 1 },
|
||||||
|
[field2Id]: { value: [1, 2] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ id: field1Id, label: "Field 1" },
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
options: [
|
||||||
|
{ id: null, label: "Option 1" },
|
||||||
|
{ id: null, label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const expectedResponses = [["1", "1, 2"]];
|
||||||
|
const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields });
|
||||||
|
expect(responses).toEqual(expectedResponses);
|
||||||
|
expect(headers).toEqual(["Field 1", "Field 2"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty responses", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const responsesFromDb = [{}];
|
||||||
|
|
||||||
|
const fields = [{ id: field1Id, label: "Field 1" }];
|
||||||
|
|
||||||
|
const expectedResponses = [[""]];
|
||||||
|
|
||||||
|
const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields });
|
||||||
|
expect(responses).toEqual(expectedResponses);
|
||||||
|
expect(headers).toEqual(["Field 1"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show correct header for deleted fields", () => {
|
||||||
|
const field1Id = uuidv4();
|
||||||
|
const field2Id = uuidv4();
|
||||||
|
const responsesFromDb = [
|
||||||
|
{
|
||||||
|
[field1Id]: { value: "value1" },
|
||||||
|
[field2Id]: { value: ["Option 1", "Option 2"] },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fields = [
|
||||||
|
{ id: field1Id, label: "Field 1" },
|
||||||
|
{
|
||||||
|
id: field2Id,
|
||||||
|
label: "Field 2",
|
||||||
|
deleted: true,
|
||||||
|
options: [
|
||||||
|
{ id: "option1", label: "Option 1" },
|
||||||
|
{ id: "option2", label: "Option 2" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const expectedResponses = [["value1", "Option 1, Option 2"]];
|
||||||
|
const expectedHeaders = ["Field 1", "Field 2(Deleted)"];
|
||||||
|
|
||||||
|
const { responses, headers } = buildResponsesForReporting({ responsesFromDb, fields });
|
||||||
|
expect(responses).toEqual(expectedResponses);
|
||||||
|
expect(headers).toEqual(expectedHeaders);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
import type { App_RoutingForms_FormResponse } from "@calcom/prisma/client";
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
|
||||||
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
||||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||||
import type { Response } from "../types/types";
|
import type { FormResponse } from "../types/types";
|
||||||
|
import type { zodNonRouterField, zodFieldView } from "../zod";
|
||||||
import type { TReportInputSchema } from "./report.schema";
|
import type { TReportInputSchema } from "./report.schema";
|
||||||
|
|
||||||
interface ReportHandlerOptions {
|
interface ReportHandlerOptions {
|
||||||
@@ -13,6 +17,45 @@ interface ReportHandlerOptions {
|
|||||||
};
|
};
|
||||||
input: TReportInputSchema;
|
input: TReportInputSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Field = z.infer<typeof zodNonRouterField>;
|
||||||
|
|
||||||
|
function ensureStringOrStringArray(value: string | number | (string | number)[]): string | string[] {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
} else if (value instanceof Array) {
|
||||||
|
return value.map((v) => v.toString());
|
||||||
|
}
|
||||||
|
return [value.toString()];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelsFromOptionIds({
|
||||||
|
options,
|
||||||
|
optionIds,
|
||||||
|
}: {
|
||||||
|
options: NonNullable<Field["options"]>;
|
||||||
|
optionIds: string | string[];
|
||||||
|
}) {
|
||||||
|
if (optionIds instanceof Array) {
|
||||||
|
const labels = optionIds.map((optionId) => {
|
||||||
|
const foundOption = options.find((option) => option.id === optionId);
|
||||||
|
// It would mean that the optionId is actually a label which is why it isn't matching any option id.
|
||||||
|
// Fallback to optionId(i.e. label) which was the case with legacy options
|
||||||
|
if (!foundOption) {
|
||||||
|
return optionId;
|
||||||
|
}
|
||||||
|
return foundOption.label;
|
||||||
|
});
|
||||||
|
return labels;
|
||||||
|
} else {
|
||||||
|
const foundOption = options.find((option) => option.id === optionIds);
|
||||||
|
if (!foundOption) {
|
||||||
|
return [optionIds];
|
||||||
|
}
|
||||||
|
return [foundOption.label];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => {
|
export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => {
|
||||||
// Can be any prisma `where` clause
|
// Can be any prisma `where` clause
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -49,27 +92,13 @@ export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOpt
|
|||||||
take,
|
take,
|
||||||
skip,
|
skip,
|
||||||
});
|
});
|
||||||
|
|
||||||
const fields = serializedForm?.fields || [];
|
const fields = serializedForm?.fields || [];
|
||||||
const headers = fields.map((f) => f.label + (f.deleted ? "(Deleted)" : ""));
|
const { responses, headers } = buildResponsesForReporting({
|
||||||
const responses: (string | number)[][] = [];
|
responsesFromDb: rows.map((r) => r.response),
|
||||||
rows.forEach((r) => {
|
fields,
|
||||||
const rowResponses: (string | number)[] = [];
|
|
||||||
responses.push(rowResponses);
|
|
||||||
fields.forEach((field) => {
|
|
||||||
if (!r.response) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = r.response as Response;
|
|
||||||
const value = response[field.id]?.value || "";
|
|
||||||
let transformedValue;
|
|
||||||
if (value instanceof Array) {
|
|
||||||
transformedValue = value.join(", ");
|
|
||||||
} else {
|
|
||||||
transformedValue = value;
|
|
||||||
}
|
|
||||||
rowResponses.push(transformedValue);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take;
|
const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take;
|
||||||
return {
|
return {
|
||||||
headers,
|
headers,
|
||||||
@@ -79,3 +108,36 @@ export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOpt
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default reportHandler;
|
export default reportHandler;
|
||||||
|
|
||||||
|
export function buildResponsesForReporting({
|
||||||
|
responsesFromDb,
|
||||||
|
fields,
|
||||||
|
}: {
|
||||||
|
responsesFromDb: App_RoutingForms_FormResponse["response"][];
|
||||||
|
fields: Pick<z.infer<typeof zodFieldView>, "id" | "options" | "label" | "deleted">[];
|
||||||
|
}) {
|
||||||
|
const headers = fields.map((f) => f.label + (f.deleted ? "(Deleted)" : ""));
|
||||||
|
const responses: string[][] = [];
|
||||||
|
responsesFromDb.forEach((r) => {
|
||||||
|
const rowResponses: string[] = [];
|
||||||
|
responses.push(rowResponses);
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const response = r as FormResponse;
|
||||||
|
const value = response[field.id]?.value || "";
|
||||||
|
if (field.options) {
|
||||||
|
const optionIds = ensureStringOrStringArray(value);
|
||||||
|
const labels = getLabelsFromOptionIds({ options: field.options, optionIds });
|
||||||
|
rowResponses.push(labels.join(", "));
|
||||||
|
} else {
|
||||||
|
const arrayOfValues = value instanceof Array ? value : [value];
|
||||||
|
const transformedValue = arrayOfValues.join(", ");
|
||||||
|
rowResponses.push(transformedValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { responses, headers };
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
|
|||||||
import { TRPCError } from "@calcom/trpc/server";
|
import { TRPCError } from "@calcom/trpc/server";
|
||||||
|
|
||||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||||
import type { Response } from "../types/types";
|
import type { FormResponse } from "../types/types";
|
||||||
import type { TResponseInputSchema } from "./response.schema";
|
import type { TResponseInputSchema } from "./response.schema";
|
||||||
import { onFormSubmission } from "./utils";
|
import { onFormSubmission } from "./utils";
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ export const responseHandler = async ({ ctx, input }: ResponseHandlerOptions) =>
|
|||||||
|
|
||||||
await onFormSubmission(
|
await onFormSubmission(
|
||||||
{ ...serializableFormWithFields, userWithEmails },
|
{ ...serializableFormWithFields, userWithEmails },
|
||||||
dbFormResponse.response as Response
|
dbFormResponse.response as FormResponse
|
||||||
);
|
);
|
||||||
return dbFormResponse;
|
return dbFormResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -8,30 +8,97 @@ import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
|||||||
import type { Ensure } from "@calcom/types/utils";
|
import type { Ensure } from "@calcom/types/utils";
|
||||||
|
|
||||||
import type { OrderedResponses } from "../types/types";
|
import type { OrderedResponses } from "../types/types";
|
||||||
import type { Response, SerializableForm } from "../types/types";
|
import type { FormResponse, SerializableForm } from "../types/types";
|
||||||
|
|
||||||
|
type Field = NonNullable<SerializableForm<App_RoutingForms_Form>["fields"]>[number];
|
||||||
|
|
||||||
|
function isOptionsField(field: Pick<Field, "type" | "options">) {
|
||||||
|
return (field.type === "select" || field.type === "multiselect") && field.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFieldResponse({
|
||||||
|
field,
|
||||||
|
fieldResponseValue,
|
||||||
|
}: {
|
||||||
|
fieldResponseValue: FormResponse[keyof FormResponse]["value"];
|
||||||
|
field: Pick<Field, "type" | "options">;
|
||||||
|
}) {
|
||||||
|
if (!isOptionsField(field)) {
|
||||||
|
return {
|
||||||
|
value: fieldResponseValue,
|
||||||
|
response: fieldResponseValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.options) {
|
||||||
|
return {
|
||||||
|
value: fieldResponseValue,
|
||||||
|
response: fieldResponseValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const valueArray = fieldResponseValue instanceof Array ? fieldResponseValue : [fieldResponseValue];
|
||||||
|
const chosenOptions = valueArray.map((idOrLabel) => {
|
||||||
|
const foundOptionById = field.options?.find((option) => {
|
||||||
|
return option.id === idOrLabel;
|
||||||
|
});
|
||||||
|
if (foundOptionById) {
|
||||||
|
return {
|
||||||
|
label: foundOptionById.label,
|
||||||
|
id: foundOptionById.id,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
label: idOrLabel.toString(),
|
||||||
|
id: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
// value is a legacy prop that is just sending the labels which can change
|
||||||
|
value: chosenOptions.map((option) => option.label),
|
||||||
|
// response is new prop that is sending the label along with id(which doesn't change)
|
||||||
|
response: chosenOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type SelectFieldWebhookResponse = string | number | string[] | { label: string; id: string | null };
|
||||||
|
type FORM_SUBMITTED_WEBHOOK_RESPONSES = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Deprecates `value` prop as it now has both the id(that doesn't change) and the label(that can change but is human friendly)
|
||||||
|
*/
|
||||||
|
response: number | string | string[] | SelectFieldWebhookResponse | SelectFieldWebhookResponse[];
|
||||||
|
/**
|
||||||
|
* @deprecated Use `response` instead
|
||||||
|
*/
|
||||||
|
value: FormResponse[keyof FormResponse]["value"];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
export async function onFormSubmission(
|
export async function onFormSubmission(
|
||||||
form: Ensure<
|
form: Ensure<
|
||||||
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
|
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
|
||||||
"fields"
|
"fields"
|
||||||
>,
|
>,
|
||||||
response: Response
|
response: FormResponse
|
||||||
) {
|
) {
|
||||||
const fieldResponsesByName: Record<
|
const fieldResponsesByIdentifier: FORM_SUBMITTED_WEBHOOK_RESPONSES = {};
|
||||||
string,
|
|
||||||
{
|
|
||||||
value: Response[keyof Response]["value"];
|
|
||||||
}
|
|
||||||
> = {};
|
|
||||||
|
|
||||||
for (const [fieldId, fieldResponse] of Object.entries(response)) {
|
for (const [fieldId, fieldResponse] of Object.entries(response)) {
|
||||||
|
const field = form.fields.find((f) => f.id === fieldId);
|
||||||
|
if (!field) {
|
||||||
|
throw new Error(`Field with id ${fieldId} not found`);
|
||||||
|
}
|
||||||
// Use the label lowercased as the key to identify a field.
|
// Use the label lowercased as the key to identify a field.
|
||||||
const key =
|
const key =
|
||||||
form.fields.find((f) => f.id === fieldId)?.identifier ||
|
form.fields.find((f) => f.id === fieldId)?.identifier ||
|
||||||
(fieldResponse.label as keyof typeof fieldResponsesByName);
|
(fieldResponse.label as keyof typeof fieldResponsesByIdentifier);
|
||||||
fieldResponsesByName[key] = {
|
fieldResponsesByIdentifier[key] = getFieldResponse({
|
||||||
value: fieldResponse.value,
|
fieldResponseValue: fieldResponse.value,
|
||||||
};
|
field,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId, teamId } = getWebhookTargetEntity(form);
|
const { userId, teamId } = getWebhookTargetEntity(form);
|
||||||
@@ -57,14 +124,14 @@ export async function onFormSubmission(
|
|||||||
formId: form.id,
|
formId: form.id,
|
||||||
formName: form.name,
|
formName: form.name,
|
||||||
teamId: form.teamId,
|
teamId: form.teamId,
|
||||||
responses: fieldResponsesByName,
|
responses: fieldResponsesByIdentifier,
|
||||||
},
|
},
|
||||||
rootData: {
|
rootData: {
|
||||||
// Send responses unwrapped at root level for backwards compatibility
|
// Send responses unwrapped at root level for backwards compatibility
|
||||||
...Object.entries(fieldResponsesByName).reduce((acc, [key, value]) => {
|
...Object.entries(fieldResponsesByIdentifier).reduce((acc, [key, value]) => {
|
||||||
acc[key] = value.value;
|
acc[key] = value.value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, Response[keyof Response]["value"]>),
|
}, {} as Record<string, FormResponse[keyof FormResponse]["value"]>),
|
||||||
},
|
},
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
console.error(`Error executing routing form webhook`, webhook, e);
|
console.error(`Error executing routing form webhook`, webhook, e);
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@ export type RoutingForm = SerializableForm<App_RoutingForms_Form>;
|
|||||||
|
|
||||||
export type QueryBuilderUpdatedConfig = typeof QueryBuilderInitialConfig & { fields: Config["fields"] };
|
export type QueryBuilderUpdatedConfig = typeof QueryBuilderInitialConfig & { fields: Config["fields"] };
|
||||||
|
|
||||||
export type Response = Record<
|
export type FormResponse = Record<
|
||||||
// Field ID
|
// Field ID
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
@@ -20,7 +20,7 @@ export type Response = Record<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export type Fields = z.infer<typeof zodFieldsView>;
|
export type Fields = z.infer<typeof zodFieldsView>;
|
||||||
export type Field = Fields[number];
|
export type Field = NonNullable<Fields>[number];
|
||||||
export type Routes = z.infer<typeof zodRoutesView>;
|
export type Routes = z.infer<typeof zodRoutesView>;
|
||||||
export type Route = Routes[0];
|
export type Route = Routes[0];
|
||||||
export type NonRouterRoute = z.infer<typeof zodNonRouterRoute>;
|
export type NonRouterRoute = z.infer<typeof zodNonRouterRoute>;
|
||||||
@@ -55,4 +55,4 @@ export type SerializableRoute =
|
|||||||
})
|
})
|
||||||
| GlobalRoute;
|
| GlobalRoute;
|
||||||
|
|
||||||
export type OrderedResponses = Response[string][];
|
export type OrderedResponses = FormResponse[string][];
|
||||||
@@ -6,9 +6,23 @@ export const zodNonRouterField = z.object({
|
|||||||
identifier: z.string().optional(),
|
identifier: z.string().optional(),
|
||||||
placeholder: z.string().optional(),
|
placeholder: z.string().optional(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
|
/**
|
||||||
|
* @deprecated in favour of `options`
|
||||||
|
*/
|
||||||
selectText: z.string().optional(),
|
selectText: z.string().optional(),
|
||||||
required: z.boolean().optional(),
|
required: z.boolean().optional(),
|
||||||
deleted: z.boolean().optional(),
|
deleted: z.boolean().optional(),
|
||||||
|
options: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
label: z.string(),
|
||||||
|
// To keep backwards compatibility with the options generated from legacy selectText, we allow saving null as id
|
||||||
|
// It helps in differentiating whether the routing logic should consider the option.label as value or option.id as value.
|
||||||
|
// This is important for legacy routes which has option.label saved in conditions and it must keep matching with the value of the option
|
||||||
|
id: z.string().or(z.null()),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const zodRouterField = zodNonRouterField.extend({
|
export const zodRouterField = zodNonRouterField.extend({
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type { appDataSchema } from "../zod";
|
|||||||
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app, eventType }) {
|
||||||
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
const { getAppData, setAppData, disabled } = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
const siteId = getAppData("SITE_ID");
|
const siteId = getAppData("SITE_ID");
|
||||||
|
const scriptURL = getAppData("SCRIPT_URL");
|
||||||
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
const { enabled, updateEnabled } = useIsAppEnabled(app);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -19,15 +20,27 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||||||
}}
|
}}
|
||||||
switchChecked={enabled}
|
switchChecked={enabled}
|
||||||
teamId={eventType.team?.id || undefined}>
|
teamId={eventType.team?.id || undefined}>
|
||||||
<TextField
|
<fieldset className="space-y-2" disabled={disabled}>
|
||||||
disabled={disabled}
|
<TextField
|
||||||
name="Site ID"
|
disabled={disabled}
|
||||||
value={siteId}
|
name="Script URL"
|
||||||
placeholder="Enter your Site ID"
|
value={scriptURL}
|
||||||
onChange={(e) => {
|
defaultValue="https://us.umami.is/script.js"
|
||||||
setAppData("SITE_ID", e.target.value);
|
placeholder="Enter the script source URL"
|
||||||
}}
|
onChange={(e) => {
|
||||||
/>
|
setAppData("SCRIPT_URL", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled}
|
||||||
|
name="Site ID"
|
||||||
|
value={siteId}
|
||||||
|
placeholder="Enter your Site ID"
|
||||||
|
onChange={(e) => {
|
||||||
|
setAppData("SITE_ID", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</fieldset>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"tag": {
|
"tag": {
|
||||||
"scripts": [
|
"scripts": [
|
||||||
{
|
{
|
||||||
"src": "https://us.umami.is/script.js",
|
"src": "{SCRIPT_URL}",
|
||||||
"attrs": {
|
"attrs": {
|
||||||
"data-website-id": "{SITE_ID}"
|
"data-website-id": "{SITE_ID}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
|
|||||||
export const appDataSchema = eventTypeAppCardZod.merge(
|
export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
z.object({
|
z.object({
|
||||||
SITE_ID: z.string().optional(),
|
SITE_ID: z.string().optional(),
|
||||||
|
SCRIPT_URL: z.string().optional(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import type { EventStatus } from "ics";
|
||||||
|
import type { TFunction } from "next-i18next";
|
||||||
|
|
||||||
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
import generateIcsString from "./generateIcsString";
|
||||||
|
|
||||||
|
export enum GenerateIcsRole {
|
||||||
|
ATTENDEE = "attendee",
|
||||||
|
ORGANIZER = "organizer",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function generateIcsFile({
|
||||||
|
calEvent,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
t,
|
||||||
|
isRequestReschedule,
|
||||||
|
}: {
|
||||||
|
calEvent: CalendarEvent;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
role: GenerateIcsRole;
|
||||||
|
status: EventStatus;
|
||||||
|
t?: TFunction;
|
||||||
|
isRequestReschedule?: boolean;
|
||||||
|
}) {
|
||||||
|
// O365 deletes emails if the calendar event is selected. Currently no option to disable this on the web
|
||||||
|
if (
|
||||||
|
role !== GenerateIcsRole.ATTENDEE &&
|
||||||
|
calEvent.destinationCalendar &&
|
||||||
|
calEvent.destinationCalendar[0].integration === "office365_calendar"
|
||||||
|
)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: "event.ics",
|
||||||
|
content: generateIcsString({
|
||||||
|
event: calEvent,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
role,
|
||||||
|
status,
|
||||||
|
t,
|
||||||
|
isRequestReschedule,
|
||||||
|
}),
|
||||||
|
method: "REQUEST",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import { getWhen } from "@calcom/lib/CalEventParser";
|
|||||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
import { GenerateIcsRole } from "./generateIcsFile";
|
||||||
|
|
||||||
export enum BookingAction {
|
export enum BookingAction {
|
||||||
Create = "create",
|
Create = "create",
|
||||||
Cancel = "cancel",
|
Cancel = "cancel",
|
||||||
@@ -30,7 +32,7 @@ const generateIcsString = ({
|
|||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
status: EventStatus;
|
status: EventStatus;
|
||||||
role: "attendee" | "organizer";
|
role: GenerateIcsRole;
|
||||||
isRequestReschedule?: boolean;
|
isRequestReschedule?: boolean;
|
||||||
t?: TFunction;
|
t?: TFunction;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -47,7 +49,7 @@ const generateIcsString = ({
|
|||||||
|
|
||||||
const getTextBody = (title: string, subtitle: string): string => {
|
const getTextBody = (title: string, subtitle: string): string => {
|
||||||
let body: string;
|
let body: string;
|
||||||
if (isRequestReschedule && role === "attendee" && t) {
|
if (isRequestReschedule && role === GenerateIcsRole.ATTENDEE && t) {
|
||||||
body = `
|
body = `
|
||||||
${title}
|
${title}
|
||||||
${getWhen(event, t)}
|
${getWhen(event, t)}
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
|
export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail {
|
||||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("new_guests_added"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("new_guests_added"),
|
role: GenerateIcsRole.ATTENDEE,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "attendee",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: this.calEvent.organizer.email,
|
replyTo: this.calEvent.organizer.email,
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
export default class AttendeeCancelledEmail extends AttendeeScheduledEmail {
|
||||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("event_request_cancelled"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("event_request_cancelled"),
|
role: GenerateIcsRole.ATTENDEE,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CANCELLED",
|
||||||
status: "CANCELLED",
|
}),
|
||||||
role: "attendee",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: this.calEvent.organizer.email,
|
replyTo: this.calEvent.organizer.email,
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
|
export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail {
|
||||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("event_location_changed"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("event_location_changed"),
|
role: GenerateIcsRole.ATTENDEE,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "attendee",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: this.calEvent.organizer.email,
|
replyTo: this.calEvent.organizer.email,
|
||||||
|
|||||||
@@ -1,21 +1,17 @@
|
|||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
import AttendeeScheduledEmail from "./attendee-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail {
|
||||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("event_type_has_been_rescheduled"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("event_type_has_been_rescheduled"),
|
role: GenerateIcsRole.ATTENDEE,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "attendee",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
|
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { TimeFormat } from "@calcom/lib/timeFormat";
|
|||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import BaseEmail from "./_base-email";
|
import BaseEmail from "./_base-email";
|
||||||
|
|
||||||
export default class AttendeeScheduledEmail extends BaseEmail {
|
export default class AttendeeScheduledEmail extends BaseEmail {
|
||||||
@@ -33,19 +33,15 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
|||||||
const clonedCalEvent = cloneDeep(this.calEvent);
|
const clonedCalEvent = cloneDeep(this.calEvent);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.calEvent.recurringEvent?.count
|
||||||
event: this.calEvent,
|
? this.t("your_event_has_been_scheduled_recurring")
|
||||||
title: this.calEvent.recurringEvent?.count
|
: this.t("your_event_has_been_scheduled"),
|
||||||
? this.t("your_event_has_been_scheduled_recurring")
|
role: GenerateIcsRole.ATTENDEE,
|
||||||
: this.t("your_event_has_been_scheduled"),
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
role: "attendee",
|
status: "CONFIRMED",
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
to: `${this.attendee.name} <${this.attendee.email}>`,
|
to: `${this.attendee.name} <${this.attendee.email}>`,
|
||||||
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`,
|
||||||
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
|
replyTo: [...this.calEvent.attendees.map(({ email }) => email), this.calEvent.organizer.email],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
|||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "..";
|
import { renderEmail } from "..";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail {
|
export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerScheduledEmail {
|
||||||
@@ -17,19 +17,15 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
|
|||||||
const toAddresses = [this.calEvent.attendees[0].email];
|
const toAddresses = [this.calEvent.attendees[0].email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("request_reschedule_booking"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("request_reschedule_subtitle", {
|
||||||
title: this.t("request_reschedule_booking"),
|
organizer: this.calEvent.organizer.name,
|
||||||
subtitle: this.t("request_reschedule_subtitle", {
|
|
||||||
organizer: this.calEvent.organizer.name,
|
|
||||||
}),
|
|
||||||
role: "attendee",
|
|
||||||
status: "CANCELLED",
|
|
||||||
}),
|
}),
|
||||||
method: "REQUEST",
|
role: GenerateIcsRole.ATTENDEE,
|
||||||
},
|
status: "CANCELLED",
|
||||||
|
}),
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
subject: `${this.t("requested_to_reschedule_subject_attendee", {
|
subject: `${this.t("requested_to_reschedule_subject_attendee", {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
|
export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
|
||||||
@@ -9,17 +9,13 @@ export default class OrganizerAddGuestsEmail extends OrganizerScheduledEmail {
|
|||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("new_guests_added"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("new_guests_added"),
|
role: GenerateIcsRole.ORGANIZER,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "organizer",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
||||||
@@ -9,17 +9,13 @@ export default class OrganizerCancelledEmail extends OrganizerScheduledEmail {
|
|||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("event_request_cancelled"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("event_request_cancelled"),
|
status: "CANCELLED",
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
role: GenerateIcsRole.ORGANIZER,
|
||||||
status: "CANCELLED",
|
}),
|
||||||
role: "organizer",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
subject: `${this.t("event_cancelled_subject", {
|
subject: `${this.t("event_cancelled_subject", {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
|
export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmail {
|
||||||
@@ -9,17 +9,13 @@ export default class OrganizerLocationChangeEmail extends OrganizerScheduledEmai
|
|||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("event_location_changed"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("event_location_changed"),
|
role: GenerateIcsRole.ORGANIZER,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "organizer",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
|||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "..";
|
import { renderEmail } from "..";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail {
|
export default class OrganizerRequestedToRescheduleEmail extends OrganizerScheduledEmail {
|
||||||
@@ -20,21 +20,17 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
|
|||||||
const toAddresses = [this.calEvent.organizer.email];
|
const toAddresses = [this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("request_reschedule_title_organizer", {
|
||||||
event: this.calEvent,
|
attendee: this.calEvent.attendees[0].name,
|
||||||
title: this.t("request_reschedule_title_organizer", {
|
|
||||||
attendee: this.calEvent.attendees[0].name,
|
|
||||||
}),
|
|
||||||
subtitle: this.t("request_reschedule_subtitle_organizer", {
|
|
||||||
attendee: this.calEvent.attendees[0].name,
|
|
||||||
}),
|
|
||||||
role: "organizer",
|
|
||||||
status: "CANCELLED",
|
|
||||||
}),
|
}),
|
||||||
method: "REQUEST",
|
subtitle: this.t("request_reschedule_subtitle_organizer", {
|
||||||
},
|
attendee: this.calEvent.attendees[0].name,
|
||||||
|
}),
|
||||||
|
role: GenerateIcsRole.ORGANIZER,
|
||||||
|
status: "CANCELLED",
|
||||||
|
}),
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
subject: `${this.t("rescheduled_event_type_subject", {
|
subject: `${this.t("rescheduled_event_type_subject", {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||||
|
|
||||||
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
||||||
@@ -9,17 +9,13 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail {
|
|||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.t("event_type_has_been_rescheduled"),
|
||||||
event: this.calEvent,
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
title: this.t("event_type_has_been_rescheduled"),
|
role: GenerateIcsRole.ORGANIZER,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "organizer",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { TimeFormat } from "@calcom/lib/timeFormat";
|
|||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { renderEmail } from "../";
|
import { renderEmail } from "../";
|
||||||
import generateIcsString from "../lib/generateIcsString";
|
import generateIcsFile from "../lib/generateIcsFile";
|
||||||
|
import { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||||
import BaseEmail from "./_base-email";
|
import BaseEmail from "./_base-email";
|
||||||
|
|
||||||
export default class OrganizerScheduledEmail extends BaseEmail {
|
export default class OrganizerScheduledEmail extends BaseEmail {
|
||||||
@@ -31,19 +32,15 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
|||||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
icalEvent: {
|
icalEvent: generateIcsFile({
|
||||||
filename: "event.ics",
|
calEvent: this.calEvent,
|
||||||
content: generateIcsString({
|
title: this.calEvent.recurringEvent?.count
|
||||||
event: this.calEvent,
|
? this.t("new_event_scheduled_recurring")
|
||||||
title: this.calEvent.recurringEvent?.count
|
: this.t("new_event_scheduled"),
|
||||||
? this.t("new_event_scheduled_recurring")
|
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
||||||
: this.t("new_event_scheduled"),
|
role: GenerateIcsRole.ORGANIZER,
|
||||||
subtitle: this.t("emailed_you_and_any_other_attendees"),
|
status: "CONFIRMED",
|
||||||
role: "organizer",
|
}),
|
||||||
status: "CONFIRMED",
|
|
||||||
}),
|
|
||||||
method: "REQUEST",
|
|
||||||
},
|
|
||||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||||
to: toAddresses.join(","),
|
to: toAddresses.join(","),
|
||||||
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
replyTo: [this.calEvent.organizer.email, ...this.calEvent.attendees.map(({ email }) => email)],
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
|||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
seatsShowAttendees: true,
|
seatsShowAttendees: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
schedulingType: true,
|
||||||
hosts: {
|
hosts: {
|
||||||
select: {
|
select: {
|
||||||
user: true,
|
user: true,
|
||||||
@@ -357,7 +358,13 @@ async function handler(req: CustomRequest) {
|
|||||||
smsReminderNumber: bookingToDelete.smsReminderNumber,
|
smsReminderNumber: bookingToDelete.smsReminderNumber,
|
||||||
evt: {
|
evt: {
|
||||||
...evt,
|
...evt,
|
||||||
...{ eventType: { slug: bookingToDelete.eventType?.slug } },
|
...{
|
||||||
|
eventType: {
|
||||||
|
slug: bookingToDelete.eventType?.slug,
|
||||||
|
schedulingType: bookingToDelete.eventType?.schedulingType,
|
||||||
|
hosts: bookingToDelete.eventType?.hosts,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
hideBranding: !!bookingToDelete.eventType?.owner?.hideBranding,
|
hideBranding: !!bookingToDelete.eventType?.owner?.hideBranding,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
|||||||
import logger from "@calcom/lib/logger";
|
import logger from "@calcom/lib/logger";
|
||||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||||
import type { PrismaClient } from "@calcom/prisma";
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||||
@@ -135,6 +136,15 @@ export async function handleConfirmation(args: {
|
|||||||
eventType: {
|
eventType: {
|
||||||
bookingFields: Prisma.JsonValue | null;
|
bookingFields: Prisma.JsonValue | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
|
schedulingType: SchedulingType | null;
|
||||||
|
hosts: {
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
destinationCalendar?: {
|
||||||
|
primaryEmail: string | null;
|
||||||
|
} | null;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
owner: {
|
owner: {
|
||||||
hideBranding?: boolean | null;
|
hideBranding?: boolean | null;
|
||||||
} | null;
|
} | null;
|
||||||
@@ -175,6 +185,21 @@ export async function handleConfirmation(args: {
|
|||||||
select: {
|
select: {
|
||||||
slug: true,
|
slug: true,
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
|
schedulingType: true,
|
||||||
|
hosts: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
destinationCalendar: {
|
||||||
|
select: {
|
||||||
|
primaryEmail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
owner: {
|
owner: {
|
||||||
select: {
|
select: {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
@@ -220,11 +245,26 @@ export async function handleConfirmation(args: {
|
|||||||
select: {
|
select: {
|
||||||
slug: true,
|
slug: true,
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
|
schedulingType: true,
|
||||||
owner: {
|
owner: {
|
||||||
select: {
|
select: {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hosts: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
destinationCalendar: {
|
||||||
|
select: {
|
||||||
|
primaryEmail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
uid: true,
|
uid: true,
|
||||||
@@ -249,7 +289,11 @@ export async function handleConfirmation(args: {
|
|||||||
const evtOfBooking = {
|
const evtOfBooking = {
|
||||||
...evt,
|
...evt,
|
||||||
metadata: { videoCallUrl: meetingUrl },
|
metadata: { videoCallUrl: meetingUrl },
|
||||||
eventType: { slug: eventTypeSlug },
|
eventType: {
|
||||||
|
slug: eventTypeSlug,
|
||||||
|
schedulingType: updatedBookings[index].eventType?.schedulingType,
|
||||||
|
hosts: updatedBookings[index].eventType?.hosts,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
|
evtOfBooking.startTime = updatedBookings[index].startTime.toISOString();
|
||||||
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
|
evtOfBooking.endTime = updatedBookings[index].endTime.toISOString();
|
||||||
|
|||||||
@@ -1741,7 +1741,11 @@ async function handler(
|
|||||||
loggerWithEventDetails.error("Error while creating booking references", JSON.stringify({ error }));
|
loggerWithEventDetails.error("Error while creating booking references", JSON.stringify({ error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const evtWithMetadata = { ...evt, metadata, eventType: { slug: eventType.slug } };
|
const evtWithMetadata = {
|
||||||
|
...evt,
|
||||||
|
metadata,
|
||||||
|
eventType: { slug: eventType.slug, schedulingType: eventType.schedulingType, hosts: eventType.hosts },
|
||||||
|
};
|
||||||
|
|
||||||
if (!eventType.metadata?.disableStandardEmails?.all?.attendee) {
|
if (!eventType.metadata?.disableStandardEmails?.all?.attendee) {
|
||||||
await scheduleMandatoryReminder(
|
await scheduleMandatoryReminder(
|
||||||
|
|||||||
@@ -2654,7 +2654,12 @@ describe("handleNewBooking", () => {
|
|||||||
|
|
||||||
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
expectWorkflowToBeNotTriggered({ emailsToReceive: [organizer.email], emails });
|
||||||
|
|
||||||
expectAwaitingPaymentEmails({ organizer, booker, emails });
|
expectAwaitingPaymentEmails({
|
||||||
|
organizer,
|
||||||
|
booker,
|
||||||
|
emails,
|
||||||
|
subject: "complete_your_booking_subject",
|
||||||
|
});
|
||||||
expectBookingPaymentIntiatedWebhookToHaveBeenFired({
|
expectBookingPaymentIntiatedWebhookToHaveBeenFired({
|
||||||
booker,
|
booker,
|
||||||
organizer,
|
organizer,
|
||||||
|
|||||||
@@ -392,8 +392,7 @@ describe("handleNewBooking", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expectWorkflowToBeTriggered({
|
expectWorkflowToBeTriggered({
|
||||||
// emailsToReceive: [organizer.email].concat(otherTeamMembers.map(member => member.email)),
|
emailsToReceive: [organizer.email].concat(otherTeamMembers.map((member) => member.email)),
|
||||||
emailsToReceive: [organizer.email],
|
|
||||||
emails,
|
emails,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -104,7 +104,17 @@ const handleSeats = async (newSeatedBookingObject: NewSeatedBookingObject) => {
|
|||||||
await scheduleWorkflowReminders({
|
await scheduleWorkflowReminders({
|
||||||
workflows,
|
workflows,
|
||||||
smsReminderNumber: smsReminderNumber || null,
|
smsReminderNumber: smsReminderNumber || null,
|
||||||
calendarEvent: { ...evt, ...{ metadata, eventType: { slug: eventType.slug } } },
|
calendarEvent: {
|
||||||
|
...evt,
|
||||||
|
...{
|
||||||
|
metadata,
|
||||||
|
eventType: {
|
||||||
|
slug: eventType.slug,
|
||||||
|
schedulingType: eventType.schedulingType,
|
||||||
|
hosts: eventType.hosts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
isNotConfirmed: evt.requiresConfirmation || false,
|
isNotConfirmed: evt.requiresConfirmation || false,
|
||||||
isRescheduleEvent: !!rescheduleUid,
|
isRescheduleEvent: !!rescheduleUid,
|
||||||
emailAttendeeSendToOverride: bookerEmail,
|
emailAttendeeSendToOverride: bookerEmail,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import logger from "@calcom/lib/logger";
|
|||||||
import { defaultHandler } from "@calcom/lib/server";
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
|
import { SchedulingType, WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
|
||||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders";
|
import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders";
|
||||||
@@ -119,6 +119,19 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
switch (reminder.workflowStep.action) {
|
switch (reminder.workflowStep.action) {
|
||||||
case WorkflowActions.EMAIL_HOST:
|
case WorkflowActions.EMAIL_HOST:
|
||||||
sendTo = reminder.booking?.userPrimaryEmail ?? reminder.booking.user?.email;
|
sendTo = reminder.booking?.userPrimaryEmail ?? reminder.booking.user?.email;
|
||||||
|
const hosts = reminder?.booking?.eventType?.hosts
|
||||||
|
?.filter((host) =>
|
||||||
|
reminder.booking?.attendees.some((attendee) => attendee.email === host.user.email)
|
||||||
|
)
|
||||||
|
.map(({ user }) => user.destinationCalendar?.primaryEmail ?? user.email);
|
||||||
|
const schedulingType = reminder.booking.eventType?.schedulingType;
|
||||||
|
|
||||||
|
if (
|
||||||
|
hosts &&
|
||||||
|
(schedulingType === SchedulingType.COLLECTIVE || schedulingType === SchedulingType.ROUND_ROBIN)
|
||||||
|
) {
|
||||||
|
sendTo = sendTo ? [sendTo, ...hosts] : hosts;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case WorkflowActions.EMAIL_ATTENDEE:
|
case WorkflowActions.EMAIL_ATTENDEE:
|
||||||
sendTo = reminder.booking.attendees[0].email;
|
sendTo = reminder.booking.attendees[0].email;
|
||||||
|
|||||||
@@ -27,7 +27,14 @@ type PartialBooking =
|
|||||||
| "attendees"
|
| "attendees"
|
||||||
| "userPrimaryEmail"
|
| "userPrimaryEmail"
|
||||||
| "smsReminderNumber"
|
| "smsReminderNumber"
|
||||||
> & { eventType: (Partial<EventType> & { team: { parentId?: number } }) | null } & {
|
> & {
|
||||||
|
eventType:
|
||||||
|
| (Partial<EventType> & {
|
||||||
|
team: { parentId?: number };
|
||||||
|
hosts: { user: { email: string; destinationCalendar?: { primaryEmail: string } } }[] | undefined;
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
} & {
|
||||||
user: Partial<User> | null;
|
user: Partial<User> | null;
|
||||||
})
|
})
|
||||||
| null;
|
| null;
|
||||||
@@ -159,6 +166,20 @@ export const select: Prisma.WorkflowReminderSelect = {
|
|||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
title: true,
|
title: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
|
hosts: {
|
||||||
|
select: {
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
destinationCalendar: {
|
||||||
|
select: {
|
||||||
|
primaryEmail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
recurringEvent: true,
|
recurringEvent: true,
|
||||||
team: {
|
team: {
|
||||||
select: {
|
select: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types";
|
import type { Workflow, WorkflowStep } from "@calcom/features/ee/workflows/lib/types";
|
||||||
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
|
import { checkSMSRateLimit } from "@calcom/lib/checkRateLimitAndThrowError";
|
||||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||||
import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
import { SchedulingType, WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import { scheduleEmailReminder } from "./emailReminderManager";
|
import { scheduleEmailReminder } from "./emailReminderManager";
|
||||||
@@ -16,7 +16,11 @@ import { scheduleWhatsappReminder } from "./whatsappReminderManager";
|
|||||||
|
|
||||||
export type ExtendedCalendarEvent = CalendarEvent & {
|
export type ExtendedCalendarEvent = CalendarEvent & {
|
||||||
metadata?: { videoCallUrl: string | undefined };
|
metadata?: { videoCallUrl: string | undefined };
|
||||||
eventType: { slug?: string };
|
eventType: {
|
||||||
|
slug?: string;
|
||||||
|
schedulingType?: SchedulingType | null;
|
||||||
|
hosts?: { user: { email: string; destinationCalendar?: { primaryEmail: string | null } | null } }[];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type ProcessWorkflowStepParams = {
|
type ProcessWorkflowStepParams = {
|
||||||
@@ -85,6 +89,13 @@ const processWorkflowStep = async (
|
|||||||
break;
|
break;
|
||||||
case WorkflowActions.EMAIL_HOST:
|
case WorkflowActions.EMAIL_HOST:
|
||||||
sendTo = [evt.organizer?.email || ""];
|
sendTo = [evt.organizer?.email || ""];
|
||||||
|
|
||||||
|
const schedulingType = evt.eventType.schedulingType;
|
||||||
|
const isTeamEvent =
|
||||||
|
schedulingType === SchedulingType.ROUND_ROBIN || schedulingType === SchedulingType.COLLECTIVE;
|
||||||
|
if (isTeamEvent && evt.team?.members) {
|
||||||
|
sendTo = sendTo.concat(evt.team.members.map((member) => member.email));
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case WorkflowActions.EMAIL_ATTENDEE:
|
case WorkflowActions.EMAIL_ATTENDEE:
|
||||||
const attendees = !!emailAttendeeSendToOverride
|
const attendees = !!emailAttendeeSendToOverride
|
||||||
|
|||||||
@@ -121,10 +121,9 @@ export default function CreateEventTypeDialog({
|
|||||||
await router.replace(`/event-types/${eventType.id}${teamId ? "?tabName=team" : ""}`);
|
await router.replace(`/event-types/${eventType.id}${teamId ? "?tabName=team" : ""}`);
|
||||||
|
|
||||||
if (isInfiniteScrollEnabled) {
|
if (isInfiniteScrollEnabled) {
|
||||||
await utils.viewer.eventTypes.getUserEventGroups.invalidate();
|
await utils.viewer.eventTypes.getEventTypesFromGroup.fetchInfinite({
|
||||||
await utils.viewer.eventTypes.getEventTypesFromGroup.invalidate({
|
group: { teamId: eventType.teamId, parentId: eventType.parentId },
|
||||||
limit: 10,
|
limit: 10,
|
||||||
group: { teamId: eventType?.teamId, parentId: eventType?.parentId },
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await utils.viewer.eventTypes.getByViewer.invalidate();
|
await utils.viewer.eventTypes.getByViewer.invalidate();
|
||||||
|
|||||||
@@ -214,7 +214,8 @@ const useBanners = () => {
|
|||||||
|
|
||||||
const Layout = (props: LayoutProps) => {
|
const Layout = (props: LayoutProps) => {
|
||||||
const banners = useBanners();
|
const banners = useBanners();
|
||||||
|
const pathname = usePathname();
|
||||||
|
const isFullPageWithoutSidebar = pathname.startsWith("/apps/routing-forms/reporting/");
|
||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const { boot } = useIntercom();
|
const { boot } = useIntercom();
|
||||||
const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title;
|
const pageTitle = typeof props.heading === "string" && !props.title ? props.heading : props.title;
|
||||||
@@ -253,7 +254,7 @@ const Layout = (props: LayoutProps) => {
|
|||||||
<TimezoneChangeDialog />
|
<TimezoneChangeDialog />
|
||||||
|
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
{banners && !props.isPlatformUser && (
|
{banners && !props.isPlatformUser && !isFullPageWithoutSidebar && (
|
||||||
<div className="sticky top-0 z-10 w-full divide-y divide-black">
|
<div className="sticky top-0 z-10 w-full divide-y divide-black">
|
||||||
{Object.keys(banners).map((key) => {
|
{Object.keys(banners).map((key) => {
|
||||||
if (key === "teamUpgradeBanner") {
|
if (key === "teamUpgradeBanner") {
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
BookingLimitsKeyOutputType_2024_06_14,
|
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
||||||
TransformBookingLimitsSchema_2024_06_14,
|
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
BookingLimitsEnum_2024_06_14,
|
||||||
|
} from "@calcom/platform-enums/monorepo";
|
||||||
import {
|
import {
|
||||||
type CreateEventTypeInput_2024_06_14,
|
type CreateEventTypeInput_2024_06_14,
|
||||||
type Integration_2024_06_14,
|
type Integration_2024_06_14,
|
||||||
type BusinessDaysWindow_2024_06_14,
|
type BusinessDaysWindow_2024_06_14,
|
||||||
type RangeWindow_2024_06_14,
|
type RangeWindow_2024_06_14,
|
||||||
type TransformFutureBookingsLimitSchema_2024_06_14,
|
type TransformFutureBookingsLimitSchema_2024_06_14,
|
||||||
|
type BookingLimitsKeyOutputType_2024_06_14,
|
||||||
|
type TransformBookingLimitsSchema_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import {
|
|
||||||
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
|
||||||
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
|
||||||
} from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
|
|
||||||
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/interval-limits.enum";
|
|
||||||
|
|
||||||
const integrationsMapping: Record<Integration_2024_06_14, string> = {
|
const integrationsMapping: Record<Integration_2024_06_14, string> = {
|
||||||
"cal-video": "integrations:daily",
|
"cal-video": "integrations:daily",
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import {
|
||||||
|
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
||||||
|
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
||||||
|
BookingLimitsEnum_2024_06_14,
|
||||||
|
} from "@calcom/platform-enums/monorepo";
|
||||||
import type {
|
import type {
|
||||||
AddressLocation_2024_06_14,
|
AddressLocation_2024_06_14,
|
||||||
IntegrationLocation_2024_06_14,
|
IntegrationLocation_2024_06_14,
|
||||||
@@ -13,11 +18,6 @@ import type {
|
|||||||
BusinessDaysWindow_2024_06_14,
|
BusinessDaysWindow_2024_06_14,
|
||||||
BookingField_2024_06_14,
|
BookingField_2024_06_14,
|
||||||
} from "@calcom/platform-types";
|
} from "@calcom/platform-types";
|
||||||
import {
|
|
||||||
BookingWindowPeriodInputTypeEnum_2024_06_14,
|
|
||||||
BookingWindowPeriodOutputTypeEnum_2024_06_14,
|
|
||||||
} from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/booking-window.enum";
|
|
||||||
import { BookingLimitsEnum_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/inputs/enums/interval-limits.enum";
|
|
||||||
|
|
||||||
import type { transformApiEventTypeBookingFields, transformApiEventTypeLocations } from "./api-request";
|
import type { transformApiEventTypeBookingFields, transformApiEventTypeLocations } from "./api-request";
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const processResult = (
|
|||||||
case "username_exists":
|
case "username_exists":
|
||||||
return {
|
return {
|
||||||
statusCode: 418,
|
statusCode: 418,
|
||||||
message: "A user exists with that username",
|
message: "A user exists with that username or email",
|
||||||
};
|
};
|
||||||
case "is_premium":
|
case "is_premium":
|
||||||
return { statusCode: 402, message: "This is a premium username." };
|
return { statusCode: 402, message: "This is a premium username." };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import type { SetStateAction, Dispatch } from "react";
|
||||||
import { useMemo, useState, useEffect } from "react";
|
import { useMemo, useState, useEffect } from "react";
|
||||||
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
@@ -14,7 +15,6 @@ import { availabilityAsString } from "@calcom/lib/availability";
|
|||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { sortAvailabilityStrings } from "@calcom/lib/weekstart";
|
import { sortAvailabilityStrings } from "@calcom/lib/weekstart";
|
||||||
import { trpc } from "@calcom/trpc";
|
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import type { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
import type { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||||
import {
|
import {
|
||||||
@@ -32,7 +32,6 @@ import {
|
|||||||
TimezoneSelect as WebTimezoneSelect,
|
TimezoneSelect as WebTimezoneSelect,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
showToast,
|
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -97,6 +96,12 @@ type AvailabilitySettingsProps = {
|
|||||||
customClassNames?: CustomClassNames;
|
customClassNames?: CustomClassNames;
|
||||||
disableEditableHeading?: boolean;
|
disableEditableHeading?: boolean;
|
||||||
enableOverrides?: boolean;
|
enableOverrides?: boolean;
|
||||||
|
bulkUpdateModalProps?: {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
save: ({ eventTypeIds }: { eventTypeIds: number[] }) => void;
|
||||||
|
isSaving: boolean;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteDialogButton = ({
|
const DeleteDialogButton = ({
|
||||||
@@ -246,21 +251,11 @@ export function AvailabilitySettings({
|
|||||||
customClassNames,
|
customClassNames,
|
||||||
disableEditableHeading = false,
|
disableEditableHeading = false,
|
||||||
enableOverrides = false,
|
enableOverrides = false,
|
||||||
|
bulkUpdateModalProps,
|
||||||
}: AvailabilitySettingsProps) {
|
}: AvailabilitySettingsProps) {
|
||||||
const [openSidebar, setOpenSidebar] = useState(false);
|
const [openSidebar, setOpenSidebar] = useState(false);
|
||||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
|
|
||||||
const utils = trpc.useUtils();
|
|
||||||
const bulkUpdateDefaultAvailabilityMutation =
|
|
||||||
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation({
|
|
||||||
onSuccess: () => {
|
|
||||||
utils.viewer.availability.list.invalidate();
|
|
||||||
setBulkUpdateModal(false);
|
|
||||||
showToast(t("success"), "success");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<AvailabilityFormValues>({
|
const form = useForm<AvailabilityFormValues>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
...schedule,
|
...schedule,
|
||||||
@@ -353,7 +348,7 @@ export function AvailabilitySettings({
|
|||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
onChange(checked);
|
onChange(checked);
|
||||||
setBulkUpdateModal(checked);
|
bulkUpdateModalProps?.setIsOpen(checked);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -362,12 +357,12 @@ export function AvailabilitySettings({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{bulkUpdateModal && (
|
{bulkUpdateModalProps && bulkUpdateModalProps?.isOpen && (
|
||||||
<BulkEditDefaultForEventsModal
|
<BulkEditDefaultForEventsModal
|
||||||
isPending={bulkUpdateDefaultAvailabilityMutation.isPending}
|
isPending={bulkUpdateModalProps?.isSaving}
|
||||||
open={bulkUpdateModal}
|
open={bulkUpdateModalProps?.isOpen}
|
||||||
setOpen={setBulkUpdateModal}
|
setOpen={bulkUpdateModalProps.setIsOpen}
|
||||||
bulkUpdateFunction={bulkUpdateDefaultAvailabilityMutation.mutate}
|
bulkUpdateFunction={bulkUpdateModalProps?.save}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
|
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
|
||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
@@ -29,6 +30,16 @@ export const AvailabilitySettingsWebWrapper = () => {
|
|||||||
const { data: travelSchedules, isPending: isPendingTravelSchedules } =
|
const { data: travelSchedules, isPending: isPendingTravelSchedules } =
|
||||||
trpc.viewer.getTravelSchedules.useQuery();
|
trpc.viewer.getTravelSchedules.useQuery();
|
||||||
|
|
||||||
|
const [isBulkUpdateModalOpen, setIsBulkUpdateModalOpen] = useState(false);
|
||||||
|
const bulkUpdateDefaultAvailabilityMutation =
|
||||||
|
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.viewer.availability.list.invalidate();
|
||||||
|
setIsBulkUpdateModalOpen(false);
|
||||||
|
showToast(t("success"), "success");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const isDefaultSchedule = me.data?.defaultScheduleId === scheduleId;
|
const isDefaultSchedule = me.data?.defaultScheduleId === scheduleId;
|
||||||
|
|
||||||
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
|
||||||
@@ -101,6 +112,12 @@ export const AvailabilitySettingsWebWrapper = () => {
|
|||||||
...values,
|
...values,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
bulkUpdateModalProps={{
|
||||||
|
isOpen: isBulkUpdateModalOpen,
|
||||||
|
setIsOpen: setIsBulkUpdateModalOpen,
|
||||||
|
save: bulkUpdateDefaultAvailabilityMutation.mutate,
|
||||||
|
isSaving: bulkUpdateDefaultAvailabilityMutation.isPending,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -76,7 +76,7 @@ export const AppleConnect: FC<Partial<Omit<OAuthConnectProps, "redir">>> = ({
|
|||||||
<Dialog open={isDialogOpen}>
|
<Dialog open={isDialogOpen}>
|
||||||
<DialogTrigger>
|
<DialogTrigger>
|
||||||
<Button
|
<Button
|
||||||
StartIcon="calendar-days"
|
StartIcon="calendar"
|
||||||
color="primary"
|
color="primary"
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={cn("", className, isDisabled && "cursor-not-allowed", !isDisabled && "cursor-pointer")}
|
className={cn("", className, isDisabled && "cursor-not-allowed", !isDisabled && "cursor-pointer")}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^5.0.10",
|
"vite": "^5.0.10",
|
||||||
"vite-plugin-dts": "^3.7.3",
|
"vite-plugin-dts": "^3.7.3",
|
||||||
"vite-plugin-inspect": "^0.8.4"
|
"vite-plugin-inspect": "^0.8.4",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|||||||
@@ -1,59 +1,66 @@
|
|||||||
import react from "@vitejs/plugin-react-swc";
|
import react from "@vitejs/plugin-react-swc";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { resolve } from "path";
|
import { resolve } from "path";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import dts from "vite-plugin-dts";
|
import dts from "vite-plugin-dts";
|
||||||
import Inspect from "vite-plugin-inspect";
|
import Inspect from "vite-plugin-inspect";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
optimizeDeps: {
|
const env = loadEnv(mode, process.cwd(), ""); // .env inside of packages/platform/atoms
|
||||||
include: [
|
const webAppUrl = env.NEXT_PUBLIC_WEBAPP_URL ?? "https://app.cal.com";
|
||||||
"@calcom/lib",
|
return {
|
||||||
"@calcom/features",
|
optimizeDeps: {
|
||||||
"@calcom/prisma",
|
include: [
|
||||||
"@calcom/dayjs",
|
"@calcom/lib",
|
||||||
"@calcom/platform-constants",
|
"@calcom/features",
|
||||||
"@calcom/platform-types",
|
"@calcom/prisma",
|
||||||
"@calcom/platform-utils",
|
"@calcom/dayjs",
|
||||||
],
|
"@calcom/platform-constants",
|
||||||
},
|
"@calcom/platform-types",
|
||||||
plugins: [Inspect(), react(), dts({ insertTypesEntry: true })],
|
"@calcom/platform-utils",
|
||||||
build: {
|
],
|
||||||
commonjsOptions: {
|
|
||||||
include: [/@calcom\/lib/, /@calcom\/features/, /node_modules/],
|
|
||||||
},
|
},
|
||||||
lib: {
|
plugins: [Inspect(), react(), dts({ insertTypesEntry: true })],
|
||||||
entry: [resolve(__dirname, "index.ts")],
|
define: {
|
||||||
name: "CalAtoms",
|
"process.env.NEXT_PUBLIC_WEBAPP_URL": `"${webAppUrl}"`,
|
||||||
fileName: "cal-atoms",
|
|
||||||
},
|
},
|
||||||
rollupOptions: {
|
build: {
|
||||||
external: ["react", "fs", "path", "os", "react-dom"],
|
commonjsOptions: {
|
||||||
output: {
|
include: [/@calcom\/lib/, /@calcom\/features/, /node_modules/],
|
||||||
globals: {
|
},
|
||||||
react: "React",
|
lib: {
|
||||||
"react-dom": "ReactDOM",
|
entry: [resolve(__dirname, "index.ts")],
|
||||||
|
name: "CalAtoms",
|
||||||
|
fileName: "cal-atoms",
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ["react", "fs", "path", "os", "react-dom"],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
react: "React",
|
||||||
|
"react-dom": "ReactDOM",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
resolve: {
|
||||||
resolve: {
|
alias: {
|
||||||
alias: {
|
fs: resolve("../../../node_modules/rollup-plugin-node-builtins"),
|
||||||
fs: resolve("../../../node_modules/rollup-plugin-node-builtins"),
|
path: resolve("../../../node_modules/rollup-plugin-node-builtins"),
|
||||||
path: resolve("../../../node_modules/rollup-plugin-node-builtins"),
|
os: resolve("../../../node_modules/rollup-plugin-node-builtins"),
|
||||||
os: resolve("../../../node_modules/rollup-plugin-node-builtins"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
"@": path.resolve(__dirname, "./src"),
|
".prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
||||||
".prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
"@prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
||||||
"@prisma/client": path.resolve(__dirname, "../../prisma-client"),
|
"@calcom/prisma": path.resolve(__dirname, "../../prisma"),
|
||||||
"@calcom/prisma": path.resolve(__dirname, "../../prisma"),
|
"@calcom/dayjs": path.resolve(__dirname, "../../dayjs"),
|
||||||
"@calcom/dayjs": path.resolve(__dirname, "../../dayjs"),
|
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
|
||||||
"@calcom/platform-constants": path.resolve(__dirname, "../constants/index.ts"),
|
"@calcom/platform-types": path.resolve(__dirname, "../types/index.ts"),
|
||||||
"@calcom/platform-types": path.resolve(__dirname, "../types/index.ts"),
|
"@calcom/platform-utils": path.resolve(__dirname, "../constants/index.ts"),
|
||||||
"@calcom/platform-utils": path.resolve(__dirname, "../constants/index.ts"),
|
"@calcom/web/public/static/locales/en/common.json": path.resolve(
|
||||||
"@calcom/web/public/static/locales/en/common.json": path.resolve(
|
__dirname,
|
||||||
__dirname,
|
"../../../apps/web/public/static/locales/en/common.json"
|
||||||
"../../../apps/web/public/static/locales/en/common.json"
|
),
|
||||||
),
|
},
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Contains enums shared by platform api and packages
|
||||||
|
|
||||||
|
from api-v2 please import from @calcom/platform-enums
|
||||||
|
|
||||||
|
from any other package please import from @calcom/platform-enums/monorepo
|
||||||
File renamed without changes.
File renamed without changes.
Loaded 100 of 120 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user