Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fab54546e | ||
|
|
820230d61d | ||
|
|
af363e975a | ||
|
|
96571df308 | ||
|
|
9868926ec1 | ||
|
|
69ca57e45a | ||
|
|
9157e1faf3 | ||
|
|
87baf0363e | ||
|
|
4c6f242345 | ||
|
|
1c8cf62582 | ||
|
|
52e1a58904 | ||
|
|
b6cef49ee6 | ||
|
|
af94c64f46 | ||
|
|
e251a5afea | ||
|
|
62d162f001 | ||
|
|
8811ebcc33 | ||
|
|
c0529a26c4 | ||
|
|
a28d9bb840 | ||
|
|
06ec89e787 | ||
|
|
a4d733fb9d | ||
|
|
c3d56f736a | ||
|
|
c629d08a9c | ||
|
|
50a41109d0 | ||
|
|
ca5b83f876 | ||
|
|
c29e37a03f | ||
|
|
14ae664941 | ||
|
|
0949bbf167 | ||
|
|
d0a1e90199 | ||
|
|
077f29305d | ||
|
|
4c528afe65 | ||
|
|
57ed383554 | ||
|
|
d53cabd163 | ||
|
|
817d8738a9 | ||
|
|
d6dbbd1472 | ||
|
|
4021801698 | ||
|
|
b38d8e7ae4 | ||
|
|
123a39bd9a | ||
|
|
596dc3fbd7 | ||
|
|
058d58a6f3 | ||
|
|
4caec83c8f | ||
|
|
3ddef59761 | ||
|
|
c7edfd5bd3 | ||
|
|
23d1a21e23 | ||
|
|
e2c54b1fc4 | ||
|
|
622b2795f5 | ||
|
|
c0298248dc | ||
|
|
db07738514 | ||
|
|
700f0aeb35 | ||
|
|
f3fd043255 | ||
|
|
a0e4580f4e | ||
|
|
659b60aac0 | ||
|
|
7131996722 |
+3
-3
@@ -16,7 +16,7 @@
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
#
|
||||
|
||||
# To enable enterprise-only features please add your envrionment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
@@ -128,7 +128,7 @@ GOOGLE_API_CREDENTIALS=
|
||||
# Inbox to send user feedback
|
||||
SEND_FEEDBACK_EMAIL=
|
||||
|
||||
# Sengrid
|
||||
# Sendgrid
|
||||
# Used for email reminders in workflows and internal sync services
|
||||
SENDGRID_API_KEY=
|
||||
SENDGRID_EMAIL=
|
||||
@@ -262,7 +262,7 @@ ORGANIZATIONS_ENABLED=
|
||||
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
|
||||
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sing-ups with
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
|
||||
# existing organizations based on email domain address
|
||||
ORGANIZATIONS_AUTOLINK=
|
||||
|
||||
|
||||
@@ -83,11 +83,12 @@ jobs:
|
||||
**/.turbo/**
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-api-v1-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for API v1 build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-api-v1-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-api-v1-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v1 build. Skipping build."
|
||||
else
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
fi
|
||||
yarn turbo run build --filter=@calcom/api...
|
||||
shell: bash
|
||||
|
||||
@@ -49,14 +49,15 @@ jobs:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for API v2 build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-api-v2-build.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-api-v2-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for API v2 build. Skipping build."
|
||||
else
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
fi
|
||||
yarn workspace @calcom/api-v2 run generate-schemas
|
||||
rm -rf apps/api/v2/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/api-v2 run build
|
||||
shell: bash
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build atoms
|
||||
name: Build Atoms
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
@@ -28,13 +28,14 @@ jobs:
|
||||
path: |
|
||||
**/dist/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
- run: |
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-atoms-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for Atoms build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-atoms-build.outputs.cache-hit != 'true'
|
||||
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
|
||||
rm -rf packages/platform/atoms/node_modules
|
||||
yarn install
|
||||
yarn workspace @calcom/atoms run build
|
||||
shell: bash
|
||||
|
||||
@@ -26,16 +26,17 @@ jobs:
|
||||
path: |
|
||||
**/docs/**
|
||||
key: ${{ runner.os }}-${{ env.cache-name }}-${{ env.key-1 }}-${{ env.key-2 }}-${{ env.key-3 }}-${{ env.key-4 }}
|
||||
# Log cache hit
|
||||
- name: Log Cache Hit
|
||||
if: steps.cache-docs-build.outputs.cache-hit == 'true'
|
||||
run: echo "Cache hit for Docs build. Skipping build."
|
||||
- name: Run build
|
||||
if: steps.cache-docs-build.outputs.cache-hit != 'true'
|
||||
working-directory: docs
|
||||
run: |
|
||||
export NODE_OPTIONS="--max_old_space_size=8192"
|
||||
if [ ${{ steps.cache-docs-build.outputs.cache-hit }} == 'true' ]; then
|
||||
echo "Cache hit for Docs build. Skipping build."
|
||||
else
|
||||
npm install -g mintlify
|
||||
mintlify dev &
|
||||
sleep 5 # Let it run for 5 seconds
|
||||
kill $!
|
||||
fi
|
||||
npm install -g mintlify
|
||||
mintlify dev &
|
||||
sleep 5 # Let it run for 5 seconds
|
||||
kill $!
|
||||
shell: bash
|
||||
|
||||
@@ -168,7 +168,10 @@ export const schemaEventTypeReadPublic = EventType.pick({
|
||||
metadata: jsonSchema.nullable(),
|
||||
customInputs: customInputSchema.array().optional(),
|
||||
link: z.string().optional(),
|
||||
hashedLink: z.object({ link: z.string() }).optional().nullable(),
|
||||
hashedLink: z
|
||||
.array(z.object({ link: z.string() }))
|
||||
.optional()
|
||||
.default([]),
|
||||
bookingFields: eventTypeBookingFields.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"dependencies": {
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.51",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.54",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -111,8 +111,9 @@ export class BookingsController_2024_04_15 {
|
||||
@Query() queryParams: GetBookingsInput_2024_04_15
|
||||
): Promise<GetBookingsOutput_2024_04_15> {
|
||||
const { filters, cursor, limit } = queryParams;
|
||||
const bookingListingByStatus = filters?.status ?? Status_2024_04_15["upcoming"];
|
||||
const bookings = await getAllUserBookings({
|
||||
bookingListingByStatus: [filters.status],
|
||||
bookingListingByStatus: [bookingListingByStatus],
|
||||
skip: cursor ?? 0,
|
||||
take: limit ?? 10,
|
||||
filters,
|
||||
|
||||
+230
-1
@@ -915,7 +915,7 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle legacy event-types booking fields", () => {
|
||||
describe("Handle event-types booking fields", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
@@ -940,6 +940,7 @@ describe("Event types Endpoints", () => {
|
||||
{ isDefault: true, required: false, slug: "notes", type: "textarea" },
|
||||
{ isDefault: true, required: false, slug: "guests", type: "multiemail" },
|
||||
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
|
||||
{ isDefault: true, type: "phone", slug: "attendeePhoneNumber", required: false },
|
||||
];
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -1092,6 +1093,21 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1207,6 +1223,21 @@ describe("Event types Endpoints", () => {
|
||||
defaultLabel: "reason_for_reschedule",
|
||||
defaultPlaceholder: "reschedule_placeholder",
|
||||
},
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(legacyEventTypeInput, user.id);
|
||||
@@ -1234,6 +1265,204 @@ describe("Event types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should return event type with unknown bookingField", async () => {
|
||||
const unknownSystemField = {
|
||||
name: "unknown-whatever",
|
||||
type: "unknown-whatever",
|
||||
label: "your team",
|
||||
sources: [
|
||||
{
|
||||
id: "user",
|
||||
type: "user",
|
||||
label: "User",
|
||||
fieldRequired: true,
|
||||
},
|
||||
],
|
||||
editable: "user",
|
||||
required: true,
|
||||
placeholder: "FC Barcelona",
|
||||
};
|
||||
|
||||
const eventTypeInput = {
|
||||
title: "unknown field event type two",
|
||||
description: "unknown field event type description two",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "unknown-field-type-two",
|
||||
locations: [],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [unknownSystemField],
|
||||
};
|
||||
const eventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${eventType.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(unknownSystemField),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
try {
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId1);
|
||||
await eventTypesRepositoryFixture.delete(legacyEventTypeId2);
|
||||
} catch (e) {
|
||||
// Event type might have been deleted by the test
|
||||
}
|
||||
try {
|
||||
await userRepositoryFixture.delete(user.id);
|
||||
} catch (e) {
|
||||
// User might have been deleted by the test
|
||||
}
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Handle event-types locations", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let organization: Team;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
|
||||
const userEmail = "locations-event-types-test-e2e@api.com";
|
||||
const name = "bob-the-locations-builder";
|
||||
const username = name;
|
||||
let user: User;
|
||||
let legacyEventTypeId1: number;
|
||||
let legacyEventTypeId2: number;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
providers: [PrismaExceptionFilter, HttpExceptionFilter],
|
||||
imports: [AppModule, UsersModule, EventTypesModule_2024_06_14, TokensModule],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
name,
|
||||
username,
|
||||
});
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["redirect-uri"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should return integration location with link and credentialId", async () => {
|
||||
const eventTypeInput = {
|
||||
title: "event type discord",
|
||||
description: "event type description",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "discord-event-type",
|
||||
locations: [
|
||||
{
|
||||
type: "integrations:discord_video",
|
||||
link: "https://discord.com/users/100",
|
||||
credentialId: 100,
|
||||
},
|
||||
],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
legacyEventTypeId1 = legacyEventType.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
expect(fetchedEventType.locations).toEqual([
|
||||
{
|
||||
type: "integration",
|
||||
integration: "discord-video",
|
||||
link: eventTypeInput.locations[0].link,
|
||||
credentialId: eventTypeInput.locations[0].credentialId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return unsupported location", async () => {
|
||||
const eventTypeInput = {
|
||||
title: "event type not existing",
|
||||
description: "event type description",
|
||||
length: 40,
|
||||
hidden: false,
|
||||
slug: "not-existing-event-type",
|
||||
locations: [
|
||||
{
|
||||
type: "this-type-does-not-exist",
|
||||
},
|
||||
],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
bookingFields: [],
|
||||
};
|
||||
const legacyEventType = await eventTypesRepositoryFixture.create(eventTypeInput, user.id);
|
||||
legacyEventTypeId1 = legacyEventType.id;
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/event-types/${legacyEventTypeId1}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<EventTypeOutput_2024_06_14> = response.body;
|
||||
const fetchedEventType = responseBody.data;
|
||||
expect(fetchedEventType.locations).toEqual([
|
||||
{ type: "unknown", location: JSON.stringify(eventTypeInput.locations[0]) },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
|
||||
+38
-6
@@ -7,8 +7,7 @@ import {
|
||||
transformLocationsInternalToApi,
|
||||
transformBookingFieldsInternalToApi,
|
||||
parseRecurringEvent,
|
||||
TransformedLocationsSchema,
|
||||
BookingFieldsSchema,
|
||||
InternalLocationSchema,
|
||||
SystemField,
|
||||
CustomField,
|
||||
parseBookingLimit,
|
||||
@@ -20,12 +19,16 @@ import {
|
||||
transformEventTypeColorsInternalToApi,
|
||||
parseEventTypeColor,
|
||||
transformSeatsInternalToApi,
|
||||
InternalLocation,
|
||||
BookingFieldSchema,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
NoticeThresholdTransformedSchema,
|
||||
EventTypeOutput_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
OutputUnknownBookingField_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
type EventTypeRelations = {
|
||||
@@ -118,7 +121,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
const locations = this.transformLocations(databaseEventType.locations);
|
||||
const customName = databaseEventType?.eventName ?? undefined;
|
||||
const bookingFields = databaseEventType.bookingFields
|
||||
? this.transformBookingFields(BookingFieldsSchema.parse(databaseEventType.bookingFields))
|
||||
? this.transformBookingFields(databaseEventType.bookingFields)
|
||||
: [];
|
||||
const recurrence = this.transformRecurringEvent(databaseEventType.recurringEvent);
|
||||
const metadata = this.transformMetadata(databaseEventType.metadata) || {};
|
||||
@@ -190,7 +193,20 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
|
||||
transformLocations(locations: any) {
|
||||
if (!locations) return [];
|
||||
return transformLocationsInternalToApi(TransformedLocationsSchema.parse(locations));
|
||||
|
||||
const knownLocations: InternalLocation[] = [];
|
||||
const unknownLocations: OutputUnknownLocation_2024_06_14[] = [];
|
||||
|
||||
for (const location of locations) {
|
||||
const result = InternalLocationSchema.safeParse(location);
|
||||
if (result.success) {
|
||||
knownLocations.push(result.data);
|
||||
} else {
|
||||
unknownLocations.push({ type: "unknown", location: JSON.stringify(location) });
|
||||
}
|
||||
}
|
||||
|
||||
return [...transformLocationsInternalToApi(knownLocations), ...unknownLocations];
|
||||
}
|
||||
|
||||
transformDestinationCalendar(destinationCalendar?: DestinationCalendar | null) {
|
||||
@@ -201,10 +217,26 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
};
|
||||
}
|
||||
|
||||
transformBookingFields(bookingFields: (SystemField | CustomField)[] | null) {
|
||||
transformBookingFields(bookingFields: any) {
|
||||
if (!bookingFields) return [];
|
||||
|
||||
return transformBookingFieldsInternalToApi(bookingFields);
|
||||
const knownBookingFields: (SystemField | CustomField)[] = [];
|
||||
const unknownBookingFields: OutputUnknownBookingField_2024_06_14[] = [];
|
||||
|
||||
for (const bookingField of bookingFields) {
|
||||
const result = BookingFieldSchema.safeParse(bookingField);
|
||||
if (result.success) {
|
||||
knownBookingFields.push(result.data);
|
||||
} else {
|
||||
unknownBookingFields.push({
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(bookingField),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...transformBookingFieldsInternalToApi(knownBookingFields), ...unknownBookingFields];
|
||||
}
|
||||
|
||||
transformRecurringEvent(recurringEvent: any) {
|
||||
|
||||
@@ -21,19 +21,15 @@ export class ResponseInterceptor implements NestInterceptor {
|
||||
const { statusCode } = response;
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Log response in JSON format
|
||||
this.logger.log(
|
||||
JSON.stringify({
|
||||
requestId,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime,
|
||||
responseBody: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: "Outgoing Response",
|
||||
})
|
||||
);
|
||||
this.logger.log("Outgoing Response", {
|
||||
requestId,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime,
|
||||
responseBody: data,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,17 +11,14 @@ export class RequestIdMiddleware implements NestMiddleware {
|
||||
req.headers["X-Request-Id"] = requestId;
|
||||
const { method, headers, body: requestBody, baseUrl } = req;
|
||||
|
||||
this.logger.log(
|
||||
JSON.stringify({
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers,
|
||||
requestBody,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: "Incoming Request",
|
||||
})
|
||||
);
|
||||
this.logger.log("Incoming Request", {
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers,
|
||||
requestBody,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -5485,6 +5485,9 @@
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/CreateManagedUserData"
|
||||
},
|
||||
"error": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -5849,7 +5852,7 @@
|
||||
"refreshToken"
|
||||
]
|
||||
},
|
||||
"AddressLocation_2024_06_14": {
|
||||
"InputAddressLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -5871,7 +5874,7 @@
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"LinkLocation_2024_06_14": {
|
||||
"InputLinkLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -5893,7 +5896,7 @@
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"IntegrationLocation_2024_06_14": {
|
||||
"InputIntegrationLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -5905,7 +5908,8 @@
|
||||
"type": "string",
|
||||
"example": "cal-video",
|
||||
"enum": [
|
||||
"cal-video"
|
||||
"cal-video",
|
||||
"google-meet"
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -5914,7 +5918,7 @@
|
||||
"integration"
|
||||
]
|
||||
},
|
||||
"PhoneLocation_2024_06_14": {
|
||||
"InputPhoneLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -6606,7 +6610,7 @@
|
||||
"showAvailabilityCount"
|
||||
]
|
||||
},
|
||||
"AttendeeAddressLocation_2024_06_14": {
|
||||
"InputAttendeeAddressLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -6619,7 +6623,7 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"AttendeePhoneLocation_2024_06_14": {
|
||||
"InputAttendeePhoneLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -6632,7 +6636,7 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"AttendeeDefinedLocation_2024_06_14": {
|
||||
"InputAttendeeDefinedLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
@@ -6734,25 +6738,25 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/LinkLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputLinkLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputIntegrationLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputPhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeePhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeDefinedLocation_2024_06_14"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6943,6 +6947,219 @@
|
||||
"slug"
|
||||
]
|
||||
},
|
||||
"OutputAddressLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
"phone",
|
||||
"attendeeAddress",
|
||||
"attendeePhone",
|
||||
"attendeeDefined"
|
||||
],
|
||||
"example": "address",
|
||||
"description": "only allowed value for type is `address`"
|
||||
},
|
||||
"address": {
|
||||
"type": "string",
|
||||
"example": "123 Example St, City, Country"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"address",
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"OutputLinkLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
"phone",
|
||||
"attendeeAddress",
|
||||
"attendeePhone",
|
||||
"attendeeDefined"
|
||||
],
|
||||
"example": "link",
|
||||
"description": "only allowed value for type is `link`"
|
||||
},
|
||||
"link": {
|
||||
"type": "string",
|
||||
"example": "https://customvideo.com/join/123456"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"link",
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"OutputIntegrationLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
"phone",
|
||||
"attendeeAddress",
|
||||
"attendeePhone",
|
||||
"attendeeDefined",
|
||||
"conferencing",
|
||||
"unknown"
|
||||
],
|
||||
"example": "integration",
|
||||
"description": "only allowed value for type is `integration`"
|
||||
},
|
||||
"integration": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cal-video",
|
||||
"google-meet",
|
||||
"zoom",
|
||||
"whereby-video",
|
||||
"whatsapp-video",
|
||||
"webex-video",
|
||||
"telegram-video",
|
||||
"tandem",
|
||||
"sylaps-video",
|
||||
"skype-video",
|
||||
"sirius-video",
|
||||
"signal-video",
|
||||
"shimmer-video",
|
||||
"salesroom-video",
|
||||
"roam-video",
|
||||
"riverside-video",
|
||||
"ping-video",
|
||||
"office365-video",
|
||||
"mirotalk-video",
|
||||
"jitsi",
|
||||
"jelly-video",
|
||||
"jelly-conferencing",
|
||||
"huddle",
|
||||
"facetime-video",
|
||||
"element-call-video",
|
||||
"eightxeight-video",
|
||||
"discord-video",
|
||||
"demodesk-video",
|
||||
"campsite-conferencing",
|
||||
"campfire-video",
|
||||
"around-video"
|
||||
],
|
||||
"example": "cal-video"
|
||||
},
|
||||
"link": {
|
||||
"type": "string"
|
||||
},
|
||||
"credentialId": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"integration"
|
||||
]
|
||||
},
|
||||
"OutputPhoneLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
"phone",
|
||||
"attendeeAddress",
|
||||
"attendeePhone",
|
||||
"attendeeDefined"
|
||||
],
|
||||
"example": "phone",
|
||||
"description": "only allowed value for type is `phone`"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string",
|
||||
"example": "+37120993151"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"phone",
|
||||
"public"
|
||||
]
|
||||
},
|
||||
"OutputConferencingLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
"phone",
|
||||
"attendeeAddress",
|
||||
"attendeePhone",
|
||||
"attendeeDefined",
|
||||
"conferencing",
|
||||
"unknown"
|
||||
],
|
||||
"example": "conferencing",
|
||||
"description": "only allowed value for type is `conferencing`"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"OutputUnknownLocation_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
"phone",
|
||||
"attendeeAddress",
|
||||
"attendeePhone",
|
||||
"attendeeDefined",
|
||||
"conferencing",
|
||||
"unknown"
|
||||
],
|
||||
"example": "unknown",
|
||||
"description": "only allowed value for type is `unknown`"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"location"
|
||||
]
|
||||
},
|
||||
"EmailDefaultFieldOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7885,16 +8102,22 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/LinkLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputLinkLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputIntegrationLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputPhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OutputConferencingLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OutputUnknownLocation_2024_06_14"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -8218,25 +8441,25 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/LinkLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputLinkLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputIntegrationLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputPhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeePhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeDefinedLocation_2024_06_14"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9697,25 +9920,25 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/LinkLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputLinkLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputIntegrationLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputPhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeePhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeDefinedLocation_2024_06_14"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10013,16 +10236,22 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/LinkLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputLinkLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputIntegrationLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/OutputPhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OutputConferencingLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/OutputUnknownLocation_2024_06_14"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10433,25 +10662,25 @@
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/AddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/LinkLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputLinkLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/IntegrationLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputIntegrationLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputPhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeAddressLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeePhoneLocation_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
|
||||
"$ref": "#/components/schemas/InputAttendeeDefinedLocation_2024_06_14"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -11425,7 +11654,8 @@
|
||||
"RECORDING_TRANSCRIPTION_GENERATED",
|
||||
"OOO_CREATED",
|
||||
"AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW"
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
|
||||
"FORM_SUBMITTED_NO_EVENT"
|
||||
]
|
||||
},
|
||||
"active": {
|
||||
@@ -11501,7 +11731,8 @@
|
||||
"RECORDING_TRANSCRIPTION_GENERATED",
|
||||
"OOO_CREATED",
|
||||
"AFTER_HOSTS_CAL_VIDEO_NO_SHOW",
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW"
|
||||
"AFTER_GUESTS_CAL_VIDEO_NO_SHOW",
|
||||
"FORM_SUBMITTED_NO_EVENT"
|
||||
]
|
||||
},
|
||||
"active": {
|
||||
@@ -12510,7 +12741,7 @@
|
||||
},
|
||||
"bookingFieldsResponses": {
|
||||
"type": "object",
|
||||
"description": "Booking field responses.",
|
||||
"description": "Booking field responses consisting of an object with booking field slug as keys and user response as values.",
|
||||
"example": {
|
||||
"customField": "customValue"
|
||||
}
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
export { GET } from "@calcom/features/tasker/api/cron";
|
||||
|
||||
/**
|
||||
* This runs each minute and we need fresh data each time
|
||||
* @see https://nextjs.org/docs/app/building-your-application/caching#opting-out-2
|
||||
**/
|
||||
export const revalidate = 0;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/apps/getServerSideProps";
|
||||
|
||||
import AppsPage, { LayoutWrapper } from "~/apps/apps-view";
|
||||
import AppsPage from "~/apps/apps-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
@@ -14,7 +14,6 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: LayoutWrapper,
|
||||
getData: withAppDirSsr(getServerSideProps),
|
||||
Page: AppsPage,
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useRouter } from "next/navigation";
|
||||
import type { ComponentProps } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { ShellMain } from "@calcom/features/shell/Shell";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
|
||||
@@ -11,7 +11,7 @@ type AppsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
actions?: (className?: string) => JSX.Element;
|
||||
emptyStore?: boolean;
|
||||
} & Omit<ComponentProps<typeof ShellMain>, "actions">;
|
||||
} & Omit<ComponentProps<typeof Shell>, "actions">;
|
||||
|
||||
export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
|
||||
const { t } = useLocale();
|
||||
@@ -21,7 +21,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
|
||||
if (session.status === "loading") return <></>;
|
||||
return (
|
||||
<ShellMain {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
|
||||
<div className="flex flex-col xl:flex-row">
|
||||
<main className="w-full">
|
||||
{emptyStore ? (
|
||||
@@ -37,6 +37,6 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</ShellMain>
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ const isBookingReroutable = (booking: ParsedBooking): booking is ReroutableBooki
|
||||
function BookingListItem(booking: BookingItemProps) {
|
||||
const parsedBooking = buildParsedBooking(booking);
|
||||
|
||||
const { userId, userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser;
|
||||
const { userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser;
|
||||
const {
|
||||
t,
|
||||
i18n: { language },
|
||||
@@ -113,6 +113,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState<boolean>(false);
|
||||
const cardCharged = booking?.payment[0]?.success;
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
@@ -268,6 +269,17 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isBookingInPast) {
|
||||
editBookingActions.push({
|
||||
id: "no_show",
|
||||
label: t("mark_as_no_show"),
|
||||
onClick: () => {
|
||||
setIsNoShowDialogOpen(true);
|
||||
},
|
||||
icon: "eye-off" as const,
|
||||
});
|
||||
}
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
@@ -417,12 +429,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
setIsOpenDialog={setIsOpenRescheduleDialog}
|
||||
bookingUId={booking.uid}
|
||||
/>
|
||||
<ReassignDialog
|
||||
isOpenDialog={isOpenReassignDialog}
|
||||
setIsOpenDialog={setIsOpenReassignDialog}
|
||||
bookingId={booking.id}
|
||||
teamId={booking.eventType?.team?.id || 0}
|
||||
/>
|
||||
{isOpenReassignDialog && (
|
||||
<ReassignDialog
|
||||
isOpenDialog={isOpenReassignDialog}
|
||||
setIsOpenDialog={setIsOpenReassignDialog}
|
||||
bookingId={booking.id}
|
||||
teamId={booking.eventType?.team?.id || 0}
|
||||
/>
|
||||
)}
|
||||
<EditLocationDialog
|
||||
booking={booking}
|
||||
saveLocation={saveLocation}
|
||||
@@ -452,6 +466,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
timeFormat={userTimeFormat ?? null}
|
||||
/>
|
||||
)}
|
||||
{isNoShowDialogOpen && (
|
||||
<NoShowAttendeesDialog
|
||||
bookingUid={booking.uid}
|
||||
attendees={attendeeList}
|
||||
setIsOpen={setIsNoShowDialogOpen}
|
||||
isOpen={isNoShowDialogOpen}
|
||||
/>
|
||||
)}
|
||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||
<DialogContent title={t("rejection_reason_title")} description={t("rejection_reason_description")}>
|
||||
<div>
|
||||
@@ -983,7 +1005,7 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
|
||||
/>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<div className=" flex justify-end p-2">
|
||||
<div className="flex justify-end p-2 ">
|
||||
<Button
|
||||
data-testid="update-no-show"
|
||||
color="secondary"
|
||||
@@ -999,6 +1021,75 @@ const GroupedAttendees = (groupedAttendeeProps: GroupedAttendeeProps) => {
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const NoShowAttendeesDialog = ({
|
||||
attendees,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
bookingUid,
|
||||
}: {
|
||||
attendees: AttendeeProps[];
|
||||
isOpen: boolean;
|
||||
setIsOpen: (value: boolean) => void;
|
||||
bookingUid: string;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const [noShowAttendees, setNoShowAttendees] = useState(
|
||||
attendees.map((attendee) => ({
|
||||
id: attendee.id,
|
||||
email: attendee.email,
|
||||
name: attendee.name,
|
||||
noShow: attendee.noShow || false,
|
||||
}))
|
||||
);
|
||||
|
||||
const noShowMutation = trpc.viewer.markNoShow.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
const newValue = data.attendees[0];
|
||||
setNoShowAttendees((old) =>
|
||||
old.map((attendee) =>
|
||||
attendee.email === newValue.email ? { ...attendee, noShow: newValue.noShow } : attendee
|
||||
)
|
||||
);
|
||||
showToast(t(data.message), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
showToast(err.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={() => setIsOpen(false)}>
|
||||
<DialogContent title={t("mark_as_no_show_title")} description={t("no_show_description")}>
|
||||
{noShowAttendees.map((attendee) => (
|
||||
<form
|
||||
key={attendee.id}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
noShowMutation.mutate({
|
||||
bookingUid,
|
||||
attendees: [{ email: attendee.email, noShow: !attendee.noShow }],
|
||||
});
|
||||
}}>
|
||||
<div className="bg-muted flex items-center justify-between rounded-md px-4 py-2">
|
||||
<span className="text-emphasis flex flex-col text-sm">
|
||||
{attendee.name}
|
||||
{attendee.email && <span className="text-muted">({attendee.email})</span>}
|
||||
</span>
|
||||
<Button color="minimal" type="submit" StartIcon={attendee.noShow ? "eye-off" : "eye"}>
|
||||
{attendee.noShow ? t("unmark_as_no_show") : t("mark_as_no_show")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
))}
|
||||
<DialogFooter>
|
||||
<DialogClose>{t("done")}</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => {
|
||||
const [openDropdown, setOpenDropdown] = useState(false);
|
||||
const { t } = useLocale();
|
||||
@@ -1035,7 +1126,7 @@ const GroupedGuests = ({ guests }: { guests: AttendeeProps[] }) => {
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<div className=" flex justify-end space-x-2 p-2">
|
||||
<div className="flex justify-end space-x-2 p-2 ">
|
||||
<Link href={`mailto:${selectedEmail}`}>
|
||||
<Button
|
||||
color="secondary"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { components } from "react-select";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
ConfirmationDialogContent,
|
||||
showToast,
|
||||
Select,
|
||||
RadioGroup as RadioArea,
|
||||
@@ -51,12 +53,14 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
{
|
||||
label: "Loading...",
|
||||
value: 0,
|
||||
status: "unavailable",
|
||||
},
|
||||
];
|
||||
|
||||
return teamMembers.data?.map((member) => ({
|
||||
label: member.name,
|
||||
value: member.id,
|
||||
status: member.status,
|
||||
}));
|
||||
}, [teamMembers]);
|
||||
|
||||
@@ -96,12 +100,19 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
},
|
||||
});
|
||||
|
||||
const [showConfirmation, setShowConfirmation] = useState(false);
|
||||
|
||||
const handleSubmit = (values: FormValues) => {
|
||||
if (values.reassignType === "round_robin") {
|
||||
roundRobinReassignMutation.mutate({ teamId, bookingId });
|
||||
} else {
|
||||
if (values.teamMemberId) {
|
||||
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId: values.teamMemberId });
|
||||
const selectedMember = teamMemberOptions?.find((member) => member.value === values.teamMemberId);
|
||||
if (selectedMember && selectedMember.status === "unavailable") {
|
||||
setShowConfirmation(true);
|
||||
} else {
|
||||
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId: values.teamMemberId });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -109,58 +120,99 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
const watchedReassignType = form.watch("reassignType");
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent title={t("reassign_round_robin_host")} description={t("reassign_to_another_rr_host")}>
|
||||
{/* TODO add team member reassignment*/}
|
||||
<Form form={form} handleSubmit={handleSubmit} ref={animationParentRef}>
|
||||
<RadioArea.Group
|
||||
onValueChange={(val) => {
|
||||
form.setValue("reassignType", val as "team_member" | "round_robin");
|
||||
}}
|
||||
className={classNames("mt-1 flex flex-col gap-4")}>
|
||||
<RadioArea.Item
|
||||
value="round_robin"
|
||||
className={classNames("w-full text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value="team_member"
|
||||
className={classNames("text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("team_member_round_robin_reassign")}</strong>
|
||||
<p>{t("team_member_round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
<>
|
||||
<Dialog
|
||||
open={isOpenDialog}
|
||||
onOpenChange={(open) => {
|
||||
setIsOpenDialog(open);
|
||||
}}>
|
||||
<DialogContent
|
||||
title={t("reassign_round_robin_host")}
|
||||
description={t("reassign_to_another_rr_host")}
|
||||
enableOverflow>
|
||||
{/* TODO add team member reassignment*/}
|
||||
<Form form={form} handleSubmit={handleSubmit} ref={animationParentRef}>
|
||||
<RadioArea.Group
|
||||
onValueChange={(val) => {
|
||||
form.setValue("reassignType", val as "team_member" | "round_robin");
|
||||
}}
|
||||
className={classNames("mt-1 flex flex-col gap-4")}>
|
||||
<RadioArea.Item
|
||||
value="round_robin"
|
||||
className={classNames("w-full text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value="team_member"
|
||||
className={classNames("text-sm")}
|
||||
classNames={{ container: classNames("w-full") }}>
|
||||
<strong className="mb-1 block">{t("team_member_round_robin_reassign")}</strong>
|
||||
<p>{t("team_member_round_robin_reassign_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
|
||||
{watchedReassignType === "team_member" && (
|
||||
<div className="mb-2">
|
||||
<Label className="text-emphasis mt-6">{t("select_team_member")}</Label>
|
||||
<Select
|
||||
value={teamMemberOptions?.find((option) => option.value === form.getValues("teamMemberId"))}
|
||||
options={teamMemberOptions}
|
||||
onChange={(event) => {
|
||||
form.setValue("teamMemberId", event?.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending || roundRobinManualReassignMutation.isPending}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{watchedReassignType === "team_member" && (
|
||||
<div className="mb-2">
|
||||
<Label className="text-emphasis mt-6">{t("select_team_member")}</Label>
|
||||
<Select
|
||||
value={teamMemberOptions?.find((option) => option.value === form.getValues("teamMemberId"))}
|
||||
options={teamMemberOptions}
|
||||
onChange={(event) => {
|
||||
form.setValue("teamMemberId", event?.value);
|
||||
}}
|
||||
components={{
|
||||
Option: ({ children, ...props }) => {
|
||||
const status = props.data?.status;
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className={classNames(
|
||||
"h-2 w-2 rounded-full",
|
||||
status === "available" ? "bg-green-500" : "bg-red-500"
|
||||
)}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</components.Option>
|
||||
);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
type="submit"
|
||||
data-testid="rejection-confirm"
|
||||
loading={roundRobinReassignMutation.isPending || roundRobinManualReassignMutation.isPending}>
|
||||
{t("reassign")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showConfirmation} onOpenChange={setShowConfirmation}>
|
||||
<ConfirmationDialogContent
|
||||
variety="warning"
|
||||
title={t("confirm_reassign_unavailable")}
|
||||
confirmBtnText={t("yes_reassign")}
|
||||
cancelBtnText={t("cancel")}
|
||||
onConfirm={() => {
|
||||
const teamMemberId = form.getValues("teamMemberId");
|
||||
if (!teamMemberId) {
|
||||
return;
|
||||
}
|
||||
roundRobinManualReassignMutation.mutate({ bookingId, teamMemberId });
|
||||
setShowConfirmation(false);
|
||||
}}>
|
||||
{t("reassign_unavailable_team_member_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -278,7 +278,10 @@ const NewRoutingManager = ({
|
||||
const router = useRouter();
|
||||
const bookerUrl = useBookerUrl();
|
||||
const teamMemberIdsMatchingAttributeLogic =
|
||||
teamMembersMatchingAttributeLogic?.data?.map((member) => member.id) || null;
|
||||
teamMembersMatchingAttributeLogic?.data?.map((member) => member.id).filter((id) => {
|
||||
// We don't want to reroute to the same user who booked the booking
|
||||
return id !== booking.user?.id;
|
||||
}) || null;
|
||||
const routedFromRoutingFormReponseId = booking.routedFromRoutingFormReponse.id;
|
||||
|
||||
const bookingEventType = booking.eventType;
|
||||
@@ -696,10 +699,13 @@ const RerouteDialogContentAndFooter = ({
|
||||
}: Pick<RerouteDialogProps, "isOpenDialog" | "setIsOpenDialog"> & {
|
||||
booking: TeamEventTypeBookingToReroute;
|
||||
}) => {
|
||||
const { data: responseWithForm, isPending: isRoutingFormLoading, error: formResponseFetchError } =
|
||||
trpc.viewer.appRoutingForms.getResponseWithFormFields.useQuery({
|
||||
formResponseId: booking.routedFromRoutingFormReponse.id,
|
||||
});
|
||||
const {
|
||||
data: responseWithForm,
|
||||
isPending: isRoutingFormLoading,
|
||||
error: formResponseFetchError,
|
||||
} = trpc.viewer.appRoutingForms.getResponseWithFormFields.useQuery({
|
||||
formResponseId: booking.routedFromRoutingFormReponse.id,
|
||||
});
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -840,10 +846,7 @@ const RerouteDialogContentAndFooterWithFormResponse = ({
|
||||
<Button
|
||||
onClick={verifyRoute}
|
||||
data-testid="verify-new-route-button"
|
||||
disabled={
|
||||
reroutingState.status === ReroutingStatusEnum.REROUTING_IN_PROGRESS ||
|
||||
isResponseFromOrganizerUnpopulated
|
||||
}>
|
||||
disabled={reroutingState.status === ReroutingStatusEnum.REROUTING_IN_PROGRESS}>
|
||||
{t("verify_new_route")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -176,6 +176,11 @@ vi.mock("@calcom/trpc/react", () => ({
|
||||
name: "Matching User 1",
|
||||
email: "matching-user-1@example.com",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Matching User 2",
|
||||
email: "matching-user-2@example.com",
|
||||
},
|
||||
],
|
||||
});
|
||||
}),
|
||||
@@ -244,10 +249,6 @@ async function mockMessageFromOpenedTab({ type, data }: { type: string; data: an
|
||||
return messageReceivedPromise;
|
||||
}
|
||||
|
||||
function correctResponses() {
|
||||
fireEvent.click(screen.getByTestId("mock-form-update-response-button"));
|
||||
}
|
||||
|
||||
function expectEventTypeInfoInCurrentRouting({
|
||||
eventTypeText,
|
||||
eventTypeHref,
|
||||
@@ -281,7 +282,7 @@ function expectAttendeesInfoInCurrentRouting({ attendeesText }: { attendeesText:
|
||||
const attendeesEl = screen.getByTestId("current-routing-status-attendees");
|
||||
expect(attendeesEl).toHaveTextContent(attendeesText);
|
||||
}
|
||||
|
||||
const userWhoBooked = { id: 1, name: "Test User", email: "user@example.com" };
|
||||
const mockBooking = {
|
||||
id: 1,
|
||||
uid: "original-booking-uid",
|
||||
@@ -298,7 +299,7 @@ const mockBooking = {
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
title: "Test Event",
|
||||
},
|
||||
user: { id: 1, name: "Test User", email: "user@example.com" },
|
||||
user: userWhoBooked,
|
||||
routedFromRoutingFormReponse: { id: 1 },
|
||||
status: BookingStatus.ACCEPTED, // Add this line
|
||||
};
|
||||
@@ -348,10 +349,8 @@ describe("RerouteDialog", () => {
|
||||
// screen.logTestingPlaygroundURL()
|
||||
});
|
||||
|
||||
test("verify_new_route button is disabled when form fields are not filled", async () => {
|
||||
test("verify_new_route button is enabled even when form fields are not filled", async () => {
|
||||
render(<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />);
|
||||
expect(screen.getByText("verify_new_route")).toBeDisabled();
|
||||
correctResponses();
|
||||
expect(screen.getByText("verify_new_route")).toBeEnabled();
|
||||
});
|
||||
|
||||
@@ -383,8 +382,6 @@ describe("RerouteDialog", () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
// Enables verify_new_route button
|
||||
correctResponses();
|
||||
fireEvent.click(screen.getByText("verify_new_route"));
|
||||
|
||||
expectEventTypeInfoInReroutePreview({
|
||||
@@ -404,8 +401,6 @@ describe("RerouteDialog", () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
// Enables verify_new_route button
|
||||
correctResponses();
|
||||
clickVerifyNewRouteButton();
|
||||
clickRescheduleToTheNewEventWithDifferentTimeslotButton();
|
||||
await mockMessageFromOpenedTab({
|
||||
@@ -421,6 +416,8 @@ describe("RerouteDialog", () => {
|
||||
expect.objectContaining({
|
||||
rescheduleUid: mockBooking.uid,
|
||||
"cal.rerouting": "true",
|
||||
// Shouldn't include the user who booked the booking
|
||||
"cal.routedTeamMemberIds": "2",
|
||||
"cal.reroutingFormResponses": JSON.stringify({
|
||||
"company-size": {
|
||||
value: "small",
|
||||
@@ -443,13 +440,13 @@ describe("RerouteDialog", () => {
|
||||
render(
|
||||
<RerouteDialog isOpenDialog={true} setIsOpenDialog={mockSetIsOpenDialog} booking={mockBooking} />
|
||||
);
|
||||
// Enables verify_new_route button
|
||||
correctResponses();
|
||||
clickVerifyNewRouteButton();
|
||||
clickRescheduleWithSameTimeslotOfChosenEventButton();
|
||||
expect(mockMutateFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rescheduleUid: mockBooking.uid,
|
||||
// Shouldn't include the user who booked the booking
|
||||
routedTeamMemberIds: [2],
|
||||
reroutingFormResponses: {
|
||||
"company-size": {
|
||||
value: "small",
|
||||
|
||||
@@ -159,14 +159,14 @@ export const OAuthClientCard = ({
|
||||
</div>
|
||||
<div className="flex items-start gap-4">
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
color="primary"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/platform/oauth-clients/${id}/edit/webhooks`)}>
|
||||
Webhooks
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-subtle hover:bg-emphasis text-white"
|
||||
color="secondary"
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
onClick={() => router.push(`/settings/platform/oauth-clients/${id}/edit`)}>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import type { ChangeEventHandler } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -96,17 +95,3 @@ export default function Apps({ categories, appStore, userAdminTeams }: PageProps
|
||||
</AppsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const LayoutWrapper = (page: React.ReactElement) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell
|
||||
title={t("app_store")}
|
||||
description={t("app_store_description")}
|
||||
withoutMain={true}
|
||||
hideHeadingOnMobile>
|
||||
{page}
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -366,6 +366,8 @@ export default function Success(props: PageProps) {
|
||||
const isEventCancelled = isCancelled && !seatReferenceUid;
|
||||
const isPastBooking = isBookingInPast;
|
||||
const isRerouting = searchParams?.get("cal.rerouting") === "true";
|
||||
const isRescheduled = bookingInfo?.rescheduled;
|
||||
|
||||
const successPageHeadline = (() => {
|
||||
if (needsConfirmationAndReschedulable) {
|
||||
return isRecurringBooking ? t("booking_submitted_recurring") : t("booking_submitted");
|
||||
@@ -379,6 +381,10 @@ export default function Success(props: PageProps) {
|
||||
return t("no_longer_attending");
|
||||
}
|
||||
|
||||
if (isRescheduled) {
|
||||
return t("your_event_has_been_rescheduled");
|
||||
}
|
||||
|
||||
if (isEventCancelled) {
|
||||
return t("event_cancelled");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.6.8",
|
||||
"version": "4.6.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
import { highPerfRouter } from "@calcom/trpc/server/routers/viewer/highPerf/_router";
|
||||
|
||||
export default createNextApiHandler(highPerfRouter);
|
||||
@@ -3,12 +3,11 @@ import { getServerSideProps } from "@lib/apps/getServerSideProps";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import type { PageProps } from "~/apps/apps-view";
|
||||
import Apps, { LayoutWrapper } from "~/apps/apps-view";
|
||||
import Apps from "~/apps/apps-view";
|
||||
|
||||
const Page = (props: PageProps) => <Apps {...props} />;
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
Page.getLayout = LayoutWrapper;
|
||||
|
||||
export { getServerSideProps };
|
||||
|
||||
|
||||
@@ -39,10 +39,14 @@
|
||||
"email_not_sent": "Error occurred while sending email",
|
||||
"event_declined_subject": "Declined: {{title}} at {{date}}",
|
||||
"event_cancelled_subject": "Canceled: {{title}} at {{date}}",
|
||||
"event_reassigned_subject": "Reassigned: {{title}} at {{date}}",
|
||||
"event_request_declined": "Your event request has been declined",
|
||||
"event_request_declined_recurring": "Your recurring event request has been declined",
|
||||
"event_request_cancelled": "Your scheduled event was canceled",
|
||||
"event_request_reassigned": "Your scheduled event was reassigned",
|
||||
"event_reassigned_subtitle": "You will no longer have the event on your calendar and your round robin likelihood will not be negatively impacted",
|
||||
"organizer": "Organizer",
|
||||
"reassigned_to": "Reassigned to",
|
||||
"need_to_reschedule_or_cancel": "Need to reschedule or cancel?",
|
||||
"you_can_view_booking_details_with_this_url": "You can view the booking details from this url {{url}} and add the event to your calendar",
|
||||
"no_options_available": "No options available",
|
||||
@@ -127,6 +131,7 @@
|
||||
"your_meeting_has_been_booked": "Your meeting has been booked",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.",
|
||||
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
|
||||
"your_event_has_been_rescheduled": "Your event has been rescheduled",
|
||||
"request_reschedule_subtitle": "{{organizer}} has canceled the booking and requested you to pick another time.",
|
||||
"request_reschedule_title_organizer": "You have requested {{attendee}} to reschedule",
|
||||
"request_reschedule_subtitle_organizer": "You have canceled the booking and {{attendee}} should pick a new booking time with you.",
|
||||
@@ -2553,7 +2558,8 @@
|
||||
"no_support_needed": "No Support Needed?",
|
||||
"hide_support": "Hide Support",
|
||||
"event_ratings": "Average Ratings",
|
||||
"event_no_show": "Host No Show",
|
||||
"event_no_show": "No-Show (Host)",
|
||||
"event_no_show_guest": "No-Show (Guest)",
|
||||
"recent_ratings": "Recent ratings",
|
||||
"no_ratings": "No ratings submitted",
|
||||
"no_ratings_description": "Add a workflow with 'Rating' to collect ratings after meetings",
|
||||
@@ -2654,10 +2660,10 @@
|
||||
"no_columns_found": "No columns found",
|
||||
"salesforce_create_record_as": "On booking, add events on and new attendees as:",
|
||||
"salesforce_lead": "Lead",
|
||||
"salesforce_contact_under_account": "Contact under an account",
|
||||
"salesforce_contact_under_account": "Contact under an account",
|
||||
"salesforce_skip_entry_creation": "Skip creating {{entry}} record if they do not exist in Salesforce",
|
||||
"salesforce_if_account_does_not_exist": "If the contact does not exist under an account, create new lead from attendee",
|
||||
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
|
||||
"salesforce_create_new_contact_under_account": "Create a new contact under an account based on email domain of attendee and existing contacts",
|
||||
"mass_assign_attributes": "Mass assign attributes",
|
||||
"reroute_preview_custom_message": "It results in showing custom message. Try changing the response to route to an event",
|
||||
"reroute_preview_external_redirect": "It results in redirecting to {{externalUrl}}. Try changing the response to route to an event",
|
||||
@@ -2685,7 +2691,14 @@
|
||||
"reschedule_with_different_timeslot": "Reschedule with different timeslot",
|
||||
"we_dont_have_id_for_the_new_event": "We don't have ID for the new event. Please go to the Routing Form and save it again.",
|
||||
"rerouted_booking_successfully_redirecting_to_booking_page": "Rerouted booking successfully. Redirecting to booking page",
|
||||
"on_booking_write_to_event_object": "On booking, write to event object",
|
||||
"field_name": "Field name",
|
||||
"add_new_field": "Add new field",
|
||||
"you_dont_have_access_to_reroute_this_booking": "You don't have access to reroute this booking",
|
||||
"form_response_not_found": "Form response not found",
|
||||
"confirm_reassign_unavailable": "Manual Reassign to unavailable host",
|
||||
"reassign_unavailable_team_member_description": "Are you sure you want to reassign this booking to an unavailable host?",
|
||||
"yes_reassign": "Yes, reassign",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,88 +1,28 @@
|
||||
import { expect, beforeEach, afterEach, it, vi, describe } from "vitest";
|
||||
|
||||
import { filterByCities, addCitiesToDropdown, handleOptionLabel } from "@calcom/lib/timezone";
|
||||
import { addTimezonesToDropdown, handleOptionLabel, filterBySearchText } from "@calcom/lib/timezone";
|
||||
|
||||
const cityData = [
|
||||
{
|
||||
city: "San Francisco",
|
||||
timezone: "America/Argentina/Cordoba",
|
||||
},
|
||||
{
|
||||
city: "Sao Francisco do Sul",
|
||||
timezone: "America/Sao_Paulo",
|
||||
},
|
||||
{
|
||||
city: "San Francisco de Macoris",
|
||||
timezone: "America/Santo_Domingo",
|
||||
},
|
||||
{
|
||||
city: "San Francisco Gotera",
|
||||
timezone: "America/El_Salvador",
|
||||
},
|
||||
{
|
||||
city: "San Francisco",
|
||||
timezone: "America/Los_Angeles",
|
||||
},
|
||||
{
|
||||
city: "Eastern Time - US & Canada",
|
||||
timezone: "America/New_York",
|
||||
},
|
||||
{
|
||||
city: "Pacific Time - US & Canada",
|
||||
timezone: "America/Los_Angeles",
|
||||
},
|
||||
{
|
||||
city: "Central Time - US & Canada",
|
||||
timezone: "America/Chicago",
|
||||
},
|
||||
{
|
||||
city: "Mountain Time - US & Canada",
|
||||
timezone: "America/Denver",
|
||||
},
|
||||
{
|
||||
city: "Atlantic Time - Canada",
|
||||
timezone: "America/Halifax",
|
||||
},
|
||||
{
|
||||
city: "Eastern European Time",
|
||||
timezone: "Europe/Bucharest",
|
||||
},
|
||||
{
|
||||
city: "Central European Time",
|
||||
timezone: "Europe/Berlin",
|
||||
},
|
||||
{
|
||||
city: "Western European Time",
|
||||
timezone: "Europe/London",
|
||||
},
|
||||
{
|
||||
city: "Australian Eastern Time",
|
||||
timezone: "Australia/Sydney",
|
||||
},
|
||||
{
|
||||
city: "Japan Standard Time",
|
||||
timezone: "Asia/Tokyo",
|
||||
},
|
||||
{
|
||||
city: "India Standard Time",
|
||||
timezone: "Asia/Kolkata",
|
||||
},
|
||||
{
|
||||
city: "Gulf Standard Time",
|
||||
timezone: "Asia/Dubai",
|
||||
},
|
||||
{
|
||||
city: "South Africa Standard Time",
|
||||
timezone: "Africa/Johannesburg",
|
||||
},
|
||||
{
|
||||
city: "Brazil Time",
|
||||
timezone: "America/Sao_Paulo",
|
||||
},
|
||||
{
|
||||
city: "Hawaii-Aleutian Standard Time",
|
||||
timezone: "Pacific/Honolulu",
|
||||
},
|
||||
const timezonesFixture = [
|
||||
{ label: "San Francisco", timezone: "America/Argentina/Cordoba" },
|
||||
{ label: "San Francisco", timezone: "America/Los_Angeles" },
|
||||
{ label: "Sao Francisco do Sul", timezone: "America/Sao_Paulo" },
|
||||
{ label: "San Francisco de Macoris", timezone: "America/Santo_Domingo" },
|
||||
{ label: "San Francisco Gotera", timezone: "America/El_Salvador" },
|
||||
{ label: "Eastern Time - US & Canada", timezone: "America/New_York" },
|
||||
{ label: "Pacific Time - US & Canada", timezone: "America/Los_Angeles" },
|
||||
{ label: "Central Time - US & Canada", timezone: "America/Chicago" },
|
||||
{ label: "Mountain Time - US & Canada", timezone: "America/Denver" },
|
||||
{ label: "Atlantic Time - Canada", timezone: "America/Halifax" },
|
||||
{ label: "Eastern European Time", timezone: "Europe/Bucharest" },
|
||||
{ label: "Central European Time", timezone: "Europe/Berlin" },
|
||||
{ label: "Western European Time", timezone: "Europe/London" },
|
||||
{ label: "Australian Eastern Time", timezone: "Australia/Sydney" },
|
||||
{ label: "Japan Standard Time", timezone: "Asia/Tokyo" },
|
||||
{ label: "India Standard Time", timezone: "Asia/Kolkata" },
|
||||
{ label: "Gulf Standard Time", timezone: "Asia/Dubai" },
|
||||
{ label: "South Africa Standard Time", timezone: "Africa/Johannesburg" },
|
||||
{ label: "Brazil Time", timezone: "America/Sao_Paulo" },
|
||||
{ label: "Hawaii-Aleutian Standard Time", timezone: "Pacific/Honolulu" },
|
||||
];
|
||||
|
||||
const option = {
|
||||
@@ -104,34 +44,34 @@ describe("getTimezone", () => {
|
||||
});
|
||||
|
||||
it("should return empty array for an empty string", () => {
|
||||
expect(filterByCities("", cityData)).toMatchInlineSnapshot(`[]`);
|
||||
expect(filterBySearchText("", timezonesFixture)).toMatchInlineSnapshot(`[]`);
|
||||
});
|
||||
|
||||
it("should filter cities for a valid city name", () => {
|
||||
expect(filterByCities("San Francisco", cityData)).toMatchInlineSnapshot(`
|
||||
expect(filterBySearchText("San Francisco", timezonesFixture)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"city": "San Francisco",
|
||||
"label": "San Francisco",
|
||||
"timezone": "America/Argentina/Cordoba",
|
||||
},
|
||||
{
|
||||
"city": "San Francisco de Macoris",
|
||||
"label": "San Francisco",
|
||||
"timezone": "America/Los_Angeles",
|
||||
},
|
||||
{
|
||||
"label": "San Francisco de Macoris",
|
||||
"timezone": "America/Santo_Domingo",
|
||||
},
|
||||
{
|
||||
"city": "San Francisco Gotera",
|
||||
"label": "San Francisco Gotera",
|
||||
"timezone": "America/El_Salvador",
|
||||
},
|
||||
{
|
||||
"city": "San Francisco",
|
||||
"timezone": "America/Los_Angeles",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it("should return appropriate timezone(s) for a given city name array", () => {
|
||||
expect(addCitiesToDropdown(cityData)).toMatchInlineSnapshot(`
|
||||
expect(addTimezonesToDropdown(timezonesFixture)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"Africa/Johannesburg": "South Africa Standard Time",
|
||||
"America/Argentina/Cordoba": "San Francisco",
|
||||
@@ -156,9 +96,9 @@ describe("getTimezone", () => {
|
||||
});
|
||||
|
||||
it("should render city name as option label if cityData is not empty", () => {
|
||||
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
|
||||
expect(handleOptionLabel(option, timezonesFixture)).toMatchInlineSnapshot(`"San Francisco GMT -8:00"`);
|
||||
vi.setSystemTime(new Date("2020-06-01"));
|
||||
expect(handleOptionLabel(option, cityData)).toMatchInlineSnapshot(`"San Francisco GMT -7:00"`);
|
||||
expect(handleOptionLabel(option, timezonesFixture)).toMatchInlineSnapshot(`"San Francisco GMT -7:00"`);
|
||||
});
|
||||
|
||||
it("should return timezone as option label if cityData is empty", () => {
|
||||
|
||||
@@ -14,11 +14,8 @@
|
||||
}
|
||||
],
|
||||
"functions": {
|
||||
"pages/api/trpc/public/[trpc].ts": {
|
||||
"memory": 1024
|
||||
},
|
||||
"pages/api/trpc/slots/[trpc].ts": {
|
||||
"memory": 1024
|
||||
"pages/api/trpc/highPerf/[trpc].ts": {
|
||||
"memory": 3008
|
||||
},
|
||||
"pages/api/trpc/appRoutingForms/[trpc].ts": {
|
||||
"memory": 2048
|
||||
|
||||
@@ -16,7 +16,7 @@ Here is what you need to be able to run Cal.com.
|
||||
- PostgreSQL
|
||||
- Yarn (recommended)
|
||||
|
||||
Cal.com can be set up and run on your machine locally. Please take a look at [this quick guide](https://cal.com/documentation/getting-started/local-development) for instructions on setting it up.
|
||||
Cal.com can be set up and run on your machine locally. Please take a look at [this quick guide](https://cal.com/docs/developing/local-development) for instructions on setting it up.
|
||||
|
||||
|
||||
<iframe
|
||||
|
||||
@@ -19,6 +19,7 @@ import cron_config_json from "./cron/config.json";
|
||||
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
|
||||
import deel_config_json from "./deel/config.json";
|
||||
import demodesk_config_json from "./demodesk/config.json";
|
||||
import dialpad_config_json from "./dialpad/config.json";
|
||||
import discord_config_json from "./discord/config.json";
|
||||
import eightxeight_config_json from "./eightxeight/config.json";
|
||||
import element_call_config_json from "./element-call/config.json";
|
||||
@@ -117,6 +118,7 @@ export const appStoreMetadata = {
|
||||
dailyvideo: dailyvideo__metadata_ts,
|
||||
deel: deel_config_json,
|
||||
demodesk: demodesk_config_json,
|
||||
dialpad: dialpad_config_json,
|
||||
discord: discord_config_json,
|
||||
eightxeight: eightxeight_config_json,
|
||||
"element-call": element_call_config_json,
|
||||
|
||||
@@ -19,6 +19,7 @@ export const apiHandlers = {
|
||||
cron: import("./cron/api"),
|
||||
deel: import("./deel/api"),
|
||||
demodesk: import("./demodesk/api"),
|
||||
dialpad: import("./dialpad/api"),
|
||||
discord: import("./discord/api"),
|
||||
eightxeight: import("./eightxeight/api"),
|
||||
"element-call": import("./element-call/api"),
|
||||
|
||||
@@ -7,6 +7,7 @@ import campfire_config_json from "./campfire/config.json";
|
||||
import campsite_config_json from "./campsite/config.json";
|
||||
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
|
||||
import demodesk_config_json from "./demodesk/config.json";
|
||||
import dialpad_config_json from "./dialpad/config.json";
|
||||
import discord_config_json from "./discord/config.json";
|
||||
import eightxeight_config_json from "./eightxeight/config.json";
|
||||
import element_call_config_json from "./element-call/config.json";
|
||||
@@ -52,6 +53,7 @@ export const appStoreMetadata = {
|
||||
campsite: campsite_config_json,
|
||||
dailyvideo: dailyvideo__metadata_ts,
|
||||
demodesk: demodesk_config_json,
|
||||
dialpad: dialpad_config_json,
|
||||
discord: discord_config_json,
|
||||
eightxeight: eightxeight_config_json,
|
||||
"element-call": element_call_config_json,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
items:
|
||||
- 1.jpeg
|
||||
- 2.jpeg
|
||||
- 3.jpeg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import appConfig from "../config.json";
|
||||
|
||||
const handler: AppDeclarativeHandler = {
|
||||
appType: appConfig.type,
|
||||
variant: appConfig.variant,
|
||||
slug: appConfig.slug,
|
||||
supportsMultipleInstalls: false,
|
||||
handlerType: "add",
|
||||
createCredential: ({ appType, user, slug, teamId }) =>
|
||||
createDefaultInstallation({ appType, user: user, slug, key: {}, teamId }),
|
||||
};
|
||||
|
||||
export default handler;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as add } from "./add";
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "dialpad",
|
||||
"slug": "dialpad",
|
||||
"type": "dialpad_conferencing",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://meetings.dialpad.com/",
|
||||
"variant": "conferencing",
|
||||
"categories": [
|
||||
"conferencing"
|
||||
],
|
||||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"appData": {
|
||||
"location": {
|
||||
"type": "integrations:{SLUG}_video",
|
||||
"label": "{TITLE}",
|
||||
"linkType": "static",
|
||||
"organizerInputPlaceholder": "https://meetings.dialpad.com/adb2002",
|
||||
"urlRegExp": "^https:\\/\\/meetings\\.dialpad\\.com\\/[a-zA-Z0-9]+$"
|
||||
}
|
||||
},
|
||||
"description": "A new way to meet, with built-in Ai",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true,
|
||||
"__template": "event-type-location-video-static"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as api from "./api";
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/dialpad",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "A new way to meet, with built-in Ai"
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 25.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.0" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 1014 719" style="enable-background:new 0 0 1014 719;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#6C3DFF;}
|
||||
.st1{fill:none;}
|
||||
</style>
|
||||
<path class="st0" d="M682.3,180.4c-42.6,0-83.7,15.7-115.6,44c-32.2,28.6-53.1,67.9-58.9,110.5V88c0-1.1-0.6-2.1-1.7-2.5
|
||||
c-1-0.4-2.2-0.2-2.9,0.6l-103,108.3c-21.7-9.3-45-14.1-68.6-14.1c-97.3,0-176.2,80.2-176.2,179.1s78.9,179.2,176.2,179.2
|
||||
c42.6,0,83.7-15.7,115.6-44c32.2-28.6,53.1-67.9,58.9-110.5V631c0,1.5,1.1,2.7,2.6,2.7c0.7,0,1.5-0.3,2-0.8l103.1-108.3
|
||||
c21.7,9.3,45,14.1,68.6,14.1c97.3,0,176.2-80.2,176.2-179.2S779.6,180.4,682.3,180.4z"/>
|
||||
<rect x="114" y="58" class="st1" width="786" height="603"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 944 B |
@@ -42,11 +42,9 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
|
||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||
|
||||
let key;
|
||||
|
||||
if (code) {
|
||||
const token = await oAuth2Client.getToken(code);
|
||||
key = token.tokens;
|
||||
const key = token.tokens;
|
||||
const grantedScopes = token.tokens.scope?.split(" ") ?? [];
|
||||
// Check if we have granted all required permissions
|
||||
const hasMissingRequiredScopes = REQUIRED_SCOPES.some((scope) => !grantedScopes.includes(scope));
|
||||
@@ -75,7 +73,12 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||
});
|
||||
|
||||
const cals = await calendar.calendarList.list({ fields: "items(id,summary,primary,accessRole)" });
|
||||
let primaryCal = cals.data.items?.find((cal) => cal.primary);
|
||||
const tokenInfo = key.access_token ? await oAuth2Client.getTokenInfo(key.access_token) : null;
|
||||
const userEmail = tokenInfo?.email ?? null;
|
||||
|
||||
let primaryCal = cals.data.items?.find(
|
||||
(cal) => cal.primary || cal.accessRole === "owner" || cal.id === userEmail
|
||||
);
|
||||
if (!primaryCal?.id) {
|
||||
// If the primary calendar is not set, set it to the first calendar
|
||||
primaryCal = cals.data.items?.[0];
|
||||
|
||||
@@ -15,6 +15,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: ["John"],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -48,6 +49,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: [],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -84,6 +86,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: [],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -110,6 +113,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: [[]],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -136,6 +140,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: [undefined, undefined],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -162,6 +167,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: [null],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -183,6 +189,7 @@ describe("queryValueValidationSchema", () => {
|
||||
field: "name",
|
||||
operator: "equal",
|
||||
value: [""],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -209,4 +216,42 @@ describe("queryValueValidationSchema", () => {
|
||||
const result2 = queryValueSaveValidationSchema.safeParse(undefined);
|
||||
expect(result2.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow fields with no value if valueSrc is empty", () => {
|
||||
const result = queryValueSaveValidationSchema.safeParse({
|
||||
id: "7",
|
||||
type: "group",
|
||||
children1: {
|
||||
rule1: {
|
||||
type: "rule",
|
||||
properties: {
|
||||
field: "name",
|
||||
operator: "is_empty",
|
||||
value: [],
|
||||
valueSrc: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should not allow fields with no value if valueSrc is not empty", () => {
|
||||
const result = queryValueSaveValidationSchema.safeParse({
|
||||
id: "7",
|
||||
type: "group",
|
||||
children1: {
|
||||
rule1: {
|
||||
type: "rule",
|
||||
properties: {
|
||||
field: "name",
|
||||
operator: "is_empty",
|
||||
value: [],
|
||||
valueSrc: ["value"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -323,11 +323,16 @@ test.describe("Routing Forms", () => {
|
||||
"Multi Select",
|
||||
"Legacy Select",
|
||||
"Select",
|
||||
// TODO: Find a way to incorporate Routed To and Booked At into the report
|
||||
// @see https://github.com/calcom/cal.com/pull/17229
|
||||
"Routed To",
|
||||
"Booked At",
|
||||
]);
|
||||
/* Last two columns are "Routed To" and "Booked At" */
|
||||
expect(responses).toEqual([
|
||||
["event-routing", "Option-2", "Option-2", "Option-2", "Option-2"],
|
||||
["external-redirect", "Option-2", "Option-2", "Option-2", "Option-2"],
|
||||
["custom-page", "Option-2", "Option-2", "Option-2", "Option-2"],
|
||||
["event-routing", "Option-2", "Option-2", "Option-2", "Option-2", "", ""],
|
||||
["external-redirect", "Option-2", "Option-2", "Option-2", "Option-2", "", ""],
|
||||
["custom-page", "Option-2", "Option-2", "Option-2", "Option-2", "", ""],
|
||||
]);
|
||||
|
||||
await page.goto(`apps/routing-forms/route-builder/${routingForm.id}`);
|
||||
|
||||
@@ -4,6 +4,7 @@ import logger from "@calcom/lib/logger";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
import type { App_RoutingForms_FormResponse } from "@calcom/prisma/client";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
|
||||
import { getSerializableForm } from "../lib/getSerializableForm";
|
||||
@@ -15,11 +16,23 @@ import type { TReportInputSchema } from "./report.schema";
|
||||
interface ReportHandlerOptions {
|
||||
ctx: {
|
||||
prisma: PrismaClient;
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TReportInputSchema;
|
||||
}
|
||||
|
||||
export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => {
|
||||
const makeFormatDate = (locale: string, timeZone: string) => {
|
||||
const formatDate = (date: Date): string => {
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
timeZone,
|
||||
dateStyle: "medium",
|
||||
timeStyle: "short",
|
||||
}).format(date);
|
||||
};
|
||||
return formatDate;
|
||||
};
|
||||
|
||||
const getRows = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => {
|
||||
// Can be any prisma `where` clause
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const prismaWhere: Record<string, any> = input.jsonLogicQuery
|
||||
@@ -32,6 +45,32 @@ export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOpt
|
||||
input.jsonLogicQuery
|
||||
)}`
|
||||
);
|
||||
const rows = await prisma.app_RoutingForms_FormResponse.findMany({
|
||||
where: {
|
||||
formId: input.formId,
|
||||
...prismaWhere,
|
||||
},
|
||||
include: {
|
||||
routedToBooking: {
|
||||
select: {
|
||||
createdAt: true,
|
||||
user: {
|
||||
select: { id: true, name: true, email: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
take,
|
||||
skip,
|
||||
});
|
||||
return { skip, take, rows };
|
||||
};
|
||||
|
||||
export const reportHandler = async (options: ReportHandlerOptions) => {
|
||||
const {
|
||||
ctx: { prisma },
|
||||
input,
|
||||
} = options;
|
||||
const form = await prisma.app_RoutingForms_Form.findUnique({
|
||||
where: {
|
||||
id: input.formId,
|
||||
@@ -46,15 +85,7 @@ export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOpt
|
||||
}
|
||||
// TODO: Second argument is required to return deleted operators.
|
||||
const serializedForm = await getSerializableForm({ form, withDeletedFields: true });
|
||||
|
||||
const rows = await prisma.app_RoutingForms_FormResponse.findMany({
|
||||
where: {
|
||||
formId: input.formId,
|
||||
...prismaWhere,
|
||||
},
|
||||
take,
|
||||
skip,
|
||||
});
|
||||
const { rows, skip, take } = await getRows(options);
|
||||
|
||||
const fields = serializedForm?.fields || [];
|
||||
const { responses, headers } = buildResponsesForReporting({
|
||||
@@ -63,13 +94,43 @@ export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOpt
|
||||
});
|
||||
|
||||
const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take;
|
||||
return {
|
||||
return presenter({
|
||||
rows,
|
||||
options,
|
||||
headers,
|
||||
responses,
|
||||
nextCursor: areThereNoResultsOrLessThanAskedFor ? null : skip + rows.length,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* This is a temporary solution to make the report work. It should be incorporated into the report data itself.
|
||||
* Right now we cannot filter by Routed To and Booked At because they are not part of the response data.
|
||||
*/
|
||||
function presenter(args: {
|
||||
rows: Awaited<ReturnType<typeof getRows>>["rows"];
|
||||
options: ReportHandlerOptions;
|
||||
headers: string[];
|
||||
responses: string[][];
|
||||
nextCursor: number | null;
|
||||
}) {
|
||||
const { headers, responses, nextCursor, options, rows } = args;
|
||||
const { ctx } = options;
|
||||
const formatDate = makeFormatDate(ctx.user.locale, ctx.user.timeZone);
|
||||
return {
|
||||
nextCursor,
|
||||
headers: [...headers, "Routed To", "Booked At"],
|
||||
responses: responses.map((r, i) => {
|
||||
const currentRow = rows[i];
|
||||
return [
|
||||
...r,
|
||||
currentRow.routedToBooking?.user?.email || "",
|
||||
currentRow.routedToBooking?.createdAt ? formatDate(currentRow.routedToBooking.createdAt) : "",
|
||||
];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export default reportHandler;
|
||||
|
||||
export function buildResponsesForReporting({
|
||||
|
||||
@@ -64,6 +64,7 @@ export enum RouteActionType {
|
||||
export const routeActionTypeSchema = z.nativeEnum(RouteActionType);
|
||||
/**
|
||||
* Stricter schema for validating before saving to DB
|
||||
* It doesn't decide what will be saved, it is just to validate the data before saving
|
||||
*/
|
||||
export const queryValueSaveValidationSchema = queryValueSchema
|
||||
.omit({ children1: true })
|
||||
@@ -78,6 +79,7 @@ export const queryValueSaveValidationSchema = queryValueSchema
|
||||
field: z.any().optional(),
|
||||
operator: z.any().optional(),
|
||||
value: z.any().optional(),
|
||||
valueSrc: z.any().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
@@ -94,7 +96,13 @@ export const queryValueSaveValidationSchema = queryValueSchema
|
||||
if (!isObject(rule.properties)) return;
|
||||
|
||||
const value = rule.properties.value || [];
|
||||
if (!(value instanceof Array)) {
|
||||
const valueSrc = rule.properties.valueSrc;
|
||||
if (!(value instanceof Array) || !(valueSrc instanceof Array)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!valueSrc.length) {
|
||||
// If valueSrc is empty, value could be empty for operators like is_empty, is_not_empty
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Switch, Alert, Select } from "@calcom/ui";
|
||||
import { Switch, Alert, Select, Button, InputField, showToast } from "@calcom/ui";
|
||||
|
||||
import { SalesforceRecordEnum } from "../lib/recordEnum";
|
||||
import type { appDataSchema } from "../zod";
|
||||
@@ -25,6 +25,8 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
const createLeadIfAccountNull = getAppData("createLeadIfAccountNull");
|
||||
const createNewContactUnderAccount = getAppData("createNewContactUnderAccount");
|
||||
const createEventOn = getAppData("createEventOn") ?? SalesforceRecordEnum.CONTACT;
|
||||
const onBookingWriteToEventObject = getAppData("onBookingWriteToEventObject") ?? false;
|
||||
const onBookingWriteToEventObjectMap = getAppData("onBookingWriteToEventObjectMap") ?? {};
|
||||
const { t } = useLocale();
|
||||
|
||||
const recordOptions = [
|
||||
@@ -45,6 +47,11 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
checkOwnerOptions.find((option) => option.value === roundRobinSkipCheckRecordOn) ?? checkOwnerOptions[0]
|
||||
);
|
||||
|
||||
const [newOnBookingWriteToEventObjectField, setNewOnBookingWriteToEventObjectField] = useState({
|
||||
field: "",
|
||||
value: "",
|
||||
});
|
||||
|
||||
return (
|
||||
<AppCard
|
||||
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
|
||||
@@ -109,6 +116,98 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4">
|
||||
<Switch
|
||||
label={t("on_booking_write_to_event_object")}
|
||||
labelOnLeading
|
||||
checked={onBookingWriteToEventObject}
|
||||
onCheckedChange={(checked) => {
|
||||
setAppData("onBookingWriteToEventObject", checked);
|
||||
}}
|
||||
/>
|
||||
{onBookingWriteToEventObject ? (
|
||||
<div className="ml-2 mt-2">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>{t("field_name")}</div>
|
||||
<div>{t("value")}</div>
|
||||
</div>
|
||||
<div>
|
||||
{...Object.keys(onBookingWriteToEventObjectMap).map((key) => (
|
||||
<div className="mt-2 grid grid-cols-3 gap-4" key={key}>
|
||||
<div>
|
||||
<InputField value={key} readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<InputField value={onBookingWriteToEventObjectMap[key]} readOnly />
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
StartIcon="trash"
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const newObject = onBookingWriteToEventObjectMap;
|
||||
delete onBookingWriteToEventObjectMap[key];
|
||||
setAppData("onBookingWriteToEventObjectMap", newObject);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="mt-2 grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<InputField
|
||||
value={newOnBookingWriteToEventObjectField.field}
|
||||
onChange={(e) =>
|
||||
setNewOnBookingWriteToEventObjectField({
|
||||
...newOnBookingWriteToEventObjectField,
|
||||
field: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<InputField
|
||||
value={newOnBookingWriteToEventObjectField.value}
|
||||
onChange={(e) =>
|
||||
setNewOnBookingWriteToEventObjectField({
|
||||
...newOnBookingWriteToEventObjectField,
|
||||
value: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="mt-2"
|
||||
size="sm"
|
||||
disabled={
|
||||
!(newOnBookingWriteToEventObjectField.field && newOnBookingWriteToEventObjectField.value)
|
||||
}
|
||||
onClick={() => {
|
||||
if (
|
||||
Object.keys(onBookingWriteToEventObjectMap).includes(
|
||||
newOnBookingWriteToEventObjectField.field.trim()
|
||||
)
|
||||
) {
|
||||
showToast("Field already exists", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setAppData("onBookingWriteToEventObjectMap", {
|
||||
...onBookingWriteToEventObjectMap,
|
||||
[newOnBookingWriteToEventObjectField.field.trim()]:
|
||||
newOnBookingWriteToEventObjectField.value.trim(),
|
||||
});
|
||||
setNewOnBookingWriteToEventObjectField({ field: "", value: "" });
|
||||
}}>
|
||||
{t("add_new_field")}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN ? (
|
||||
<div className="mt-4">
|
||||
<Switch
|
||||
|
||||
@@ -171,8 +171,26 @@ export default class SalesforceCRMService implements CRM {
|
||||
};
|
||||
|
||||
private salesforceCreateEvent = async (event: CalendarEvent, contacts: Contact[]) => {
|
||||
const appOptions = this.getAppOptions();
|
||||
|
||||
const customFieldInputsEnabled =
|
||||
appOptions?.onBookingWriteToEventObject && appOptions?.onBookingWriteToEventObjectMap;
|
||||
|
||||
const customFieldInputs = customFieldInputsEnabled
|
||||
? await this.ensureFieldsExistOnObject(Object.keys(appOptions?.onBookingWriteToEventObjectMap), "Event")
|
||||
: [];
|
||||
|
||||
const confirmedCustomFieldInputs: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
|
||||
for (const field of customFieldInputs) {
|
||||
confirmedCustomFieldInputs[field] = appOptions.onBookingWriteToEventObjectMap[field];
|
||||
}
|
||||
|
||||
const createdEvent = await this.salesforceCreateEventApiCall(event, {
|
||||
EventWhoIds: contacts.map((contact) => contact.id),
|
||||
...confirmedCustomFieldInputs,
|
||||
}).catch(async (reason) => {
|
||||
if (reason === sfApiErrors.INVALID_EVENTWHOIDS) {
|
||||
this.calWarnings.push(
|
||||
@@ -388,6 +406,8 @@ export default class SalesforceCRMService implements CRM {
|
||||
|
||||
const accountId = this.getDominantAccountId(response.records as { AccountId: string }[]);
|
||||
|
||||
let contactCreated = false;
|
||||
|
||||
if (accountId && appOptions.createNewContactUnderAccount) {
|
||||
// First see if the contact already exists and connect it to the account
|
||||
const userQuery = await conn.query(`SELECT Id, Email FROM Contact WHERE Email = '${attendee.email}'`);
|
||||
@@ -414,35 +434,60 @@ export default class SalesforceCRMService implements CRM {
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
createdContacts.push({ id: result.id, email: attendee.email });
|
||||
contactCreated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (appOptions.createLeadIfAccountNull) {
|
||||
await Promise.all(
|
||||
contactsToCreate.map(async (attendee) => {
|
||||
return await conn
|
||||
.sobject(SalesforceRecordEnum.LEAD)
|
||||
.create(
|
||||
this.generateCreateRecordBody({
|
||||
attendee,
|
||||
recordType: SalesforceRecordEnum.LEAD,
|
||||
organizerId,
|
||||
})
|
||||
)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
createdContacts.push({ id: result.id, email: attendee.email });
|
||||
}
|
||||
});
|
||||
})
|
||||
);
|
||||
if (!accountId && appOptions.createLeadIfAccountNull && !contactCreated) {
|
||||
// Check to see if the lead exists already
|
||||
const leadQuery = await conn.query(`SELECT Id, Email FROM Lead WHERE Email = '${attendee.email}'`);
|
||||
if (leadQuery.records.length) {
|
||||
const contact = leadQuery.records[0] as { Id: string; Email: string };
|
||||
return [{ id: contact.Id, email: contact.Email }];
|
||||
}
|
||||
|
||||
for (const attendee of contactsToCreate) {
|
||||
try {
|
||||
const result = await conn.sobject(SalesforceRecordEnum.LEAD).create(
|
||||
this.generateCreateRecordBody({
|
||||
attendee,
|
||||
recordType: SalesforceRecordEnum.LEAD,
|
||||
organizerId,
|
||||
})
|
||||
);
|
||||
if (result.success) {
|
||||
createdContacts.push({ id: result.id, email: attendee.email });
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.name === "DUPLICATES_DETECTED") {
|
||||
const existingId = this.getExistingIdFromDuplicateError(error);
|
||||
if (existingId) {
|
||||
console.log("Using existing record:", existingId);
|
||||
createdContacts.push({ id: existingId, email: attendee.email });
|
||||
}
|
||||
} else {
|
||||
console.error("Error creating lead:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createdContacts;
|
||||
}
|
||||
|
||||
private getExistingIdFromDuplicateError(error: any): string | null {
|
||||
if (error.duplicateResult && error.duplicateResult.matchResults) {
|
||||
for (const matchResult of error.duplicateResult.matchResults) {
|
||||
if (matchResult.matchRecords && matchResult.matchRecords.length > 0) {
|
||||
return matchResult.matchRecords[0].record.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private setDoNotCreateEvent(boolean: boolean) {
|
||||
this.doNotCreateEvent = boolean;
|
||||
}
|
||||
@@ -500,4 +545,29 @@ export default class SalesforceCRMService implements CRM {
|
||||
...(recordType === SalesforceRecordEnum.LEAD && { Company: company }),
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureFieldsExistOnObject(fieldsToTest: string[], sobject: string) {
|
||||
const conn = await this.conn;
|
||||
|
||||
const fieldSet = new Set(fieldsToTest);
|
||||
const foundFields: string[] = [];
|
||||
|
||||
try {
|
||||
const salesforceEntity = await conn.describe(sobject);
|
||||
const fields = salesforceEntity.fields;
|
||||
|
||||
for (const field of fields) {
|
||||
if (foundFields.length === fieldSet.size) break;
|
||||
|
||||
if (fieldSet.has(field.name)) {
|
||||
foundFields.push(field.name);
|
||||
}
|
||||
}
|
||||
|
||||
return foundFields;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export const appDataSchema = eventTypeAppCardZod.extend({
|
||||
createEventOn: z.nativeEnum(SalesforceRecordEnum).default(SalesforceRecordEnum.CONTACT).optional(),
|
||||
createNewContactUnderAccount: z.boolean().optional(),
|
||||
createLeadIfAccountNull: z.boolean().optional(),
|
||||
onBookingWriteToEventObject: z.boolean().optional(),
|
||||
onBookingWriteToEventObjectMap: z.record(z.any()).optional(),
|
||||
});
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
|
||||
@@ -976,7 +976,7 @@ export default class EventManager {
|
||||
let success = true;
|
||||
const createdEvent = await crm.createEvent(event, currentAppOption).catch((error) => {
|
||||
success = false;
|
||||
log.warn(`Error creating crm event for ${credential.type}`, error);
|
||||
log.warn(`Error creating crm event for ${credential.type}`, JSON.stringify(error));
|
||||
});
|
||||
|
||||
if (createdEvent) {
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import type { Booking, Prisma, EventType as PrismaEventType } from "@prisma/client";
|
||||
import type {
|
||||
Booking,
|
||||
Prisma,
|
||||
OutOfOfficeEntry,
|
||||
OutOfOfficeReason,
|
||||
User,
|
||||
EventType as PrismaEventType,
|
||||
} from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
@@ -152,7 +159,7 @@ const _getUser = async (where: Prisma.UserWhereInput) => {
|
||||
});
|
||||
};
|
||||
|
||||
type User = Awaited<ReturnType<typeof getUser>>;
|
||||
type GetUser = Awaited<ReturnType<typeof getUser>>;
|
||||
|
||||
export const getCurrentSeats = async (
|
||||
...args: Parameters<typeof _getCurrentSeats>
|
||||
@@ -238,7 +245,7 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
returnDateOverrides: boolean;
|
||||
},
|
||||
initialData?: {
|
||||
user?: User;
|
||||
user?: GetUser;
|
||||
eventType?: EventType;
|
||||
currentSeats?: CurrentSeats;
|
||||
rescheduleUid?: string | null;
|
||||
@@ -251,6 +258,11 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
seatsReferences: number;
|
||||
};
|
||||
})[];
|
||||
outOfOfficeDays?: (Pick<OutOfOfficeEntry, "id" | "start" | "end"> & {
|
||||
user: Pick<User, "id" | "name">;
|
||||
toUser: Pick<User, "id" | "username" | "name"> | null;
|
||||
reason: Pick<OutOfOfficeReason, "id" | "emoji" | "reason"> | null;
|
||||
})[];
|
||||
busyTimesFromLimitsBookings: EventBusyDetails[];
|
||||
}
|
||||
) {
|
||||
@@ -297,10 +309,10 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
)[0];
|
||||
|
||||
const hostSchedule = eventType?.hosts?.find((host) => host.user.id === user.id)?.schedule;
|
||||
|
||||
|
||||
// TODO: It uses default timezone of user. Should we use timezone of team ?
|
||||
const fallbackTimezoneIfScheduleIsMissing = eventType?.timeZone || user.timeZone;
|
||||
|
||||
|
||||
const fallbackSchedule = {
|
||||
availability: [
|
||||
{
|
||||
@@ -312,10 +324,12 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
],
|
||||
id: 0,
|
||||
|
||||
timeZone: fallbackTimezoneIfScheduleIsMissing
|
||||
timeZone: fallbackTimezoneIfScheduleIsMissing,
|
||||
};
|
||||
|
||||
const schedule = (eventType?.schedule ? eventType.schedule : hostSchedule ? hostSchedule : userSchedule) ?? fallbackSchedule
|
||||
const schedule =
|
||||
(eventType?.schedule ? eventType.schedule : hostSchedule ? hostSchedule : userSchedule) ??
|
||||
fallbackSchedule;
|
||||
const timeZone = schedule?.timeZone || fallbackTimezoneIfScheduleIsMissing;
|
||||
|
||||
const bookingLimits = parseBookingLimit(eventType?.bookingLimits);
|
||||
@@ -445,12 +459,74 @@ const _getUserAvailability = async function getUsersWorkingHoursLifeTheUniverseA
|
||||
}
|
||||
}
|
||||
|
||||
const datesOutOfOffice = await getOutOfOfficeDays({
|
||||
userId: user.id,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
availability,
|
||||
});
|
||||
const outOfOfficeDays =
|
||||
initialData?.outOfOfficeDays ??
|
||||
(await prisma.outOfOfficeEntry.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
OR: [
|
||||
// outside of range
|
||||
// (start <= 'dateTo' AND end >= 'dateFrom')
|
||||
{
|
||||
start: {
|
||||
lte: dateTo.toISOString(),
|
||||
},
|
||||
end: {
|
||||
gte: dateFrom.toISOString(),
|
||||
},
|
||||
},
|
||||
// start is between dateFrom and dateTo but end is outside of range
|
||||
// (start <= 'dateTo' AND end >= 'dateTo')
|
||||
{
|
||||
start: {
|
||||
lte: dateTo.toISOString(),
|
||||
},
|
||||
|
||||
end: {
|
||||
gte: dateTo.toISOString(),
|
||||
},
|
||||
},
|
||||
// end is between dateFrom and dateTo but start is outside of range
|
||||
// (start <= 'dateFrom' OR end <= 'dateTo')
|
||||
{
|
||||
start: {
|
||||
lte: dateFrom.toISOString(),
|
||||
},
|
||||
|
||||
end: {
|
||||
lte: dateTo.toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
start: true,
|
||||
end: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
toUser: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
reason: {
|
||||
select: {
|
||||
id: true,
|
||||
emoji: true,
|
||||
reason: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const datesOutOfOffice: IOutOfOfficeData = calculateOutOfOfficeRanges(outOfOfficeDays, availability);
|
||||
|
||||
const { dateRanges, oooExcludedDateRanges } = buildDateRanges({
|
||||
dateFrom,
|
||||
@@ -520,9 +596,6 @@ const _getPeriodStartDatesBetween = (dateFrom: Dayjs, dateTo: Dayjs, period: Int
|
||||
};
|
||||
|
||||
interface GetUserAvailabilityParamsDTO {
|
||||
userId: number;
|
||||
dateFrom: Dayjs;
|
||||
dateTo: Dayjs;
|
||||
availability: (DateOverride | WorkingHours)[];
|
||||
}
|
||||
|
||||
@@ -546,83 +619,11 @@ export interface IOutOfOfficeData {
|
||||
};
|
||||
}
|
||||
|
||||
const getOutOfOfficeDays = async (
|
||||
...args: Parameters<typeof _getOutOfOfficeDays>
|
||||
): Promise<ReturnType<typeof _getOutOfOfficeDays>> => {
|
||||
return monitorCallbackAsync(_getOutOfOfficeDays, ...args);
|
||||
};
|
||||
|
||||
const _getOutOfOfficeDays = async ({
|
||||
userId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
availability,
|
||||
}: GetUserAvailabilityParamsDTO): Promise<IOutOfOfficeData> => {
|
||||
const outOfOfficeDays = await prisma.outOfOfficeEntry.findMany({
|
||||
where: {
|
||||
userId,
|
||||
OR: [
|
||||
// outside of range
|
||||
// (start <= 'dateTo' AND end >= 'dateFrom')
|
||||
{
|
||||
start: {
|
||||
lte: dateTo.toISOString(),
|
||||
},
|
||||
end: {
|
||||
gte: dateFrom.toISOString(),
|
||||
},
|
||||
},
|
||||
// start is between dateFrom and dateTo but end is outside of range
|
||||
// (start <= 'dateTo' AND end >= 'dateTo')
|
||||
{
|
||||
start: {
|
||||
lte: dateTo.toISOString(),
|
||||
},
|
||||
|
||||
end: {
|
||||
gte: dateTo.toISOString(),
|
||||
},
|
||||
},
|
||||
// end is between dateFrom and dateTo but start is outside of range
|
||||
// (start <= 'dateFrom' OR end <= 'dateTo')
|
||||
{
|
||||
start: {
|
||||
lte: dateFrom.toISOString(),
|
||||
},
|
||||
|
||||
end: {
|
||||
lte: dateTo.toISOString(),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
start: true,
|
||||
end: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
toUser: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
reason: {
|
||||
select: {
|
||||
id: true,
|
||||
emoji: true,
|
||||
reason: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!outOfOfficeDays.length) {
|
||||
const calculateOutOfOfficeRanges = (
|
||||
outOfOfficeDays: GetUserAvailabilityInitialData["outOfOfficeDays"],
|
||||
availability: GetUserAvailabilityParamsDTO["availability"]
|
||||
): IOutOfOfficeData => {
|
||||
if (!outOfOfficeDays || outOfOfficeDays.length === 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -673,6 +674,7 @@ const _getUsersAvailability = async ({
|
||||
}: {
|
||||
users: (GetAvailabilityUser & {
|
||||
currentBookings?: GetUserAvailabilityInitialData["currentBookings"];
|
||||
outOfOfficeDays?: GetUserAvailabilityInitialData["outOfOfficeDays"];
|
||||
})[];
|
||||
query: Omit<GetUserAvailabilityQuery, "userId" | "username">;
|
||||
initialData?: Omit<GetUserAvailabilityInitialData, "user">;
|
||||
@@ -690,6 +692,7 @@ const _getUsersAvailability = async ({
|
||||
...initialData,
|
||||
user,
|
||||
currentBookings: user.currentBookings,
|
||||
outOfOfficeDays: user.outOfOfficeDays,
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
|
||||
@@ -65,6 +65,7 @@ import OrganizerDailyVideoDownloadRecordingEmail from "./templates/organizer-dai
|
||||
import OrganizerDailyVideoDownloadTranscriptEmail from "./templates/organizer-daily-video-download-transcript-email";
|
||||
import OrganizerLocationChangeEmail from "./templates/organizer-location-change-email";
|
||||
import OrganizerPaymentRefundFailedEmail from "./templates/organizer-payment-refund-failed-email";
|
||||
import OrganizerReassignedEmail from "./templates/organizer-reassigned-email";
|
||||
import OrganizerRequestEmail from "./templates/organizer-request-email";
|
||||
import OrganizerRequestReminderEmail from "./templates/organizer-request-reminder-email";
|
||||
import OrganizerRequestedToRescheduleEmail from "./templates/organizer-requested-to-reschedule-email";
|
||||
@@ -191,7 +192,8 @@ export const sendRoundRobinRescheduledEmailsAndSMS = async (
|
||||
export const sendRoundRobinCancelledEmailsAndSMS = async (
|
||||
calEvent: CalendarEvent,
|
||||
members: Person[],
|
||||
eventTypeMetadata?: EventTypeMetadata
|
||||
eventTypeMetadata?: EventTypeMetadata,
|
||||
reassignedTo?: { name: string | null; email: string }
|
||||
) => {
|
||||
if (eventTypeDisableHostEmail(eventTypeMetadata)) return;
|
||||
const calendarEvent = formatCalEvent(calEvent);
|
||||
@@ -199,9 +201,16 @@ export const sendRoundRobinCancelledEmailsAndSMS = async (
|
||||
const successfullyReScheduledSMS = new EventCancelledSMS(calEvent);
|
||||
|
||||
for (const teamMember of members) {
|
||||
emailsAndSMSToSend.push(
|
||||
sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
if (!reassignedTo) {
|
||||
emailsAndSMSToSend.push(
|
||||
sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember }))
|
||||
);
|
||||
} else {
|
||||
emailsAndSMSToSend.push(
|
||||
sendEmail(() => new OrganizerReassignedEmail({ calEvent: calendarEvent, teamMember, reassignedTo }))
|
||||
);
|
||||
}
|
||||
|
||||
if (teamMember.phoneNumber) {
|
||||
emailsAndSMSToSend.push(successfullyReScheduledSMS.sendSMSToAttendee(teamMember));
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { Info } from "./Info";
|
||||
|
||||
const PersonInfo = ({ name = "", email = "", role = "", phoneNumber = "" }) => (
|
||||
export const PersonInfo = ({ name = "", email = "", role = "", phoneNumber = "" }) => (
|
||||
<div style={{ color: "#101010", fontWeight: 400, lineHeight: "24px" }}>
|
||||
{name} - {role} {phoneNumber}
|
||||
{!isSmsCalEmail(email) && (
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
AppsStatus,
|
||||
UserFieldsResponses,
|
||||
} from "../components";
|
||||
import { PersonInfo } from "../components/WhoInfo";
|
||||
|
||||
export const BaseScheduledEmail = (
|
||||
props: {
|
||||
@@ -26,6 +27,7 @@ export const BaseScheduledEmail = (
|
||||
locale: string;
|
||||
timeFormat: TimeFormat | undefined;
|
||||
isOrganizer?: boolean;
|
||||
reassignedTo?: { name: string | null; email: string };
|
||||
} & Partial<React.ComponentProps<typeof BaseEmailHtml>>
|
||||
) => {
|
||||
const { t, timeZone, locale, timeFormat: timeFormat_ } = props;
|
||||
@@ -79,7 +81,15 @@ export const BaseScheduledEmail = (
|
||||
withSpacer
|
||||
/>
|
||||
)}
|
||||
<Info label={t("rejection_reason")} description={props.calEvent.rejectionReason} withSpacer />
|
||||
{props.reassignedTo && (
|
||||
<Info
|
||||
label={t("reassigned_to")}
|
||||
description={
|
||||
<PersonInfo name={props.reassignedTo.name || undefined} email={props.reassignedTo.email} />
|
||||
}
|
||||
withSpacer
|
||||
/>
|
||||
)}
|
||||
<Info label={t("what")} description={props.calEvent.title} withSpacer />
|
||||
<WhenInfo timeFormat={timeFormat} calEvent={props.calEvent} t={t} timeZone={timeZone} locale={locale} />
|
||||
<WhoInfo calEvent={props.calEvent} t={t} />
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { OrganizerScheduledEmail } from "./OrganizerScheduledEmail";
|
||||
|
||||
export const OrganizerReassignedEmail = (props: React.ComponentProps<typeof OrganizerScheduledEmail>) => {
|
||||
const t = props.teamMember?.language.translate || props.calEvent.organizer.language.translate;
|
||||
return (
|
||||
<OrganizerScheduledEmail
|
||||
title="event_request_reassigned"
|
||||
headerType="xCircle"
|
||||
subject="event_reassigned_subject"
|
||||
subtitle={<>{t("event_reassigned_subtitle")}</>}
|
||||
callToAction={null}
|
||||
reassignedTo={props.reassignedTo}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -10,6 +10,7 @@ export const OrganizerScheduledEmail = (
|
||||
newSeat?: boolean;
|
||||
attendeeCancelled?: boolean;
|
||||
teamMember?: Person;
|
||||
reassignedTo?: { email: string; name: string | null };
|
||||
} & Partial<React.ComponentProps<typeof BaseScheduledEmail>>
|
||||
) => {
|
||||
let subject;
|
||||
@@ -49,12 +50,17 @@ export const OrganizerScheduledEmail = (
|
||||
timeFormat={timeFormat}
|
||||
isOrganizer
|
||||
subtitle={
|
||||
<>
|
||||
{props.attendeeCancelled
|
||||
? t("attendee_no_longer_attending_subtitle", { name: props.attendee.name })
|
||||
: ""}
|
||||
</>
|
||||
props.subtitle ? (
|
||||
props.subtitle
|
||||
) : (
|
||||
<>
|
||||
{props.attendeeCancelled
|
||||
? t("attendee_no_longer_attending_subtitle", { name: props.attendee.name })
|
||||
: ""}
|
||||
</>
|
||||
)
|
||||
}
|
||||
reassignedTo={props.reassignedTo}
|
||||
{...props}
|
||||
attendee={attendee}
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@ export { SlugReplacementEmail } from "./SlugReplacementEmail";
|
||||
export { FeedbackEmail } from "./FeedbackEmail";
|
||||
export { ForgotPasswordEmail } from "./ForgotPasswordEmail";
|
||||
export { OrganizerCancelledEmail } from "./OrganizerCancelledEmail";
|
||||
export { OrganizerReassignedEmail } from "./OrganizerReassignedEmail";
|
||||
export { OrganizerLocationChangeEmail } from "./OrganizerLocationChangeEmail";
|
||||
export { OrganizerPaymentRefundFailedEmail } from "./OrganizerPaymentRefundFailedEmail";
|
||||
export { OrganizerRequestEmail } from "./OrganizerRequestEmail";
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { EMAIL_FROM_NAME } from "@calcom/lib/constants";
|
||||
|
||||
import { renderEmail } from "../";
|
||||
import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile";
|
||||
import OrganizerScheduledEmail from "./organizer-scheduled-email";
|
||||
|
||||
export default class OrganizerReassignedEmail extends OrganizerScheduledEmail {
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
const toAddresses = [this.teamMember?.email || this.calEvent.organizer.email];
|
||||
|
||||
return {
|
||||
icalEvent: generateIcsFile({
|
||||
calEvent: this.calEvent,
|
||||
status: "CANCELLED",
|
||||
role: GenerateIcsRole.ORGANIZER,
|
||||
}),
|
||||
from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`,
|
||||
to: toAddresses.join(","),
|
||||
subject: `${this.t("event_reassigned_subject", {
|
||||
title: this.calEvent.title,
|
||||
date: this.getFormattedDate(),
|
||||
})}`,
|
||||
html: await renderEmail("OrganizerReassignedEmail", {
|
||||
attendee: this.calEvent.organizer,
|
||||
calEvent: this.calEvent,
|
||||
reassignedTo: this.reassignedTo,
|
||||
}),
|
||||
text: this.getTextBody("event_request_reassigned"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,21 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
||||
t: TFunction;
|
||||
newSeat?: boolean;
|
||||
teamMember?: Person;
|
||||
reassignedTo?: { name: string | null; email: string };
|
||||
|
||||
constructor(input: { calEvent: CalendarEvent; newSeat?: boolean; teamMember?: Person }) {
|
||||
constructor(input: {
|
||||
calEvent: CalendarEvent;
|
||||
newSeat?: boolean;
|
||||
teamMember?: Person;
|
||||
reassignedTo?: { email: string; name: string | null };
|
||||
}) {
|
||||
super();
|
||||
this.name = "SEND_BOOKING_CONFIRMATION";
|
||||
this.calEvent = input.calEvent;
|
||||
this.t = this.calEvent.organizer.language.translate;
|
||||
this.newSeat = input.newSeat;
|
||||
this.teamMember = input.teamMember;
|
||||
this.reassignedTo = input.reassignedTo;
|
||||
}
|
||||
|
||||
protected async getNodeMailerPayload(): Promise<Record<string, unknown>> {
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"timezones": [
|
||||
{
|
||||
"name": "Eastern Time - US & Canada",
|
||||
"identifier": "America/New_York"
|
||||
},
|
||||
{
|
||||
"name": "Pacific Time - US & Canada",
|
||||
"identifier": "America/Los_Angeles"
|
||||
},
|
||||
{
|
||||
"name": "Central Time - US & Canada",
|
||||
"identifier": "America/Chicago"
|
||||
},
|
||||
{
|
||||
"name": "Mountain Time - US & Canada",
|
||||
"identifier": "America/Denver"
|
||||
},
|
||||
{
|
||||
"name": "Atlantic Time - Canada",
|
||||
"identifier": "America/Halifax"
|
||||
},
|
||||
{
|
||||
"name": "Eastern European Time",
|
||||
"identifier": "Europe/Bucharest"
|
||||
},
|
||||
{
|
||||
"name": "Central European Time",
|
||||
"identifier": "Europe/Berlin"
|
||||
},
|
||||
{
|
||||
"name": "Western European Time",
|
||||
"identifier": "Europe/London"
|
||||
},
|
||||
{
|
||||
"name": "Australian Eastern Time",
|
||||
"identifier": "Australia/Sydney"
|
||||
},
|
||||
{
|
||||
"name": "Japan Standard Time",
|
||||
"identifier": "Asia/Tokyo"
|
||||
},
|
||||
{
|
||||
"name": "India Standard Time",
|
||||
"identifier": "Asia/Kolkata"
|
||||
},
|
||||
{
|
||||
"name": "Gulf Standard Time",
|
||||
"identifier": "Asia/Dubai"
|
||||
},
|
||||
{
|
||||
"name": "South Africa Standard Time",
|
||||
"identifier": "Africa/Johannesburg"
|
||||
},
|
||||
{
|
||||
"name": "Brazil Time",
|
||||
"identifier": "America/Sao_Paulo"
|
||||
},
|
||||
{
|
||||
"name": "Hawaii-Aleutian Standard Time",
|
||||
"identifier": "Pacific/Honolulu"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import * as additionalTimezones from "./additionalTimezones.json";
|
||||
|
||||
export interface Timezone {
|
||||
name: string;
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
export const timezones: Timezone[] = additionalTimezones.timezones;
|
||||
|
||||
export default additionalTimezones;
|
||||
@@ -179,17 +179,32 @@ const LayoutToggle = ({
|
||||
return [
|
||||
{
|
||||
value: BookerLayouts.MONTH_VIEW,
|
||||
label: <Icon name="calendar" width="16" height="16" />,
|
||||
label: (
|
||||
<>
|
||||
<Icon name="calendar" width="16" height="16" />
|
||||
<span className="sr-only">${t("switch_monthly")}</span>
|
||||
</>
|
||||
),
|
||||
tooltip: t("switch_monthly"),
|
||||
},
|
||||
{
|
||||
value: BookerLayouts.WEEK_VIEW,
|
||||
label: <Icon name="grid-3x3" width="16" height="16" />,
|
||||
label: (
|
||||
<>
|
||||
<Icon name="grid-3x3" width="16" height="16" />
|
||||
<span className="sr-only">${t("switch_weekly")}</span>
|
||||
</>
|
||||
),
|
||||
tooltip: t("switch_weekly"),
|
||||
},
|
||||
{
|
||||
value: BookerLayouts.COLUMN_VIEW,
|
||||
label: <Icon name="columns-3" width="16" height="16" />,
|
||||
label: (
|
||||
<>
|
||||
<Icon name="columns-3" width="16" height="16" />
|
||||
<span className="sr-only">${t("switch_columnview")}</span>
|
||||
</>
|
||||
),
|
||||
tooltip: t("switch_columnview"),
|
||||
},
|
||||
].filter((layout) => enabledLayouts?.includes(layout.value as BookerLayouts));
|
||||
|
||||
@@ -810,6 +810,7 @@ async function handler(
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
teamId,
|
||||
orgId,
|
||||
oAuthClientId: platformClientId,
|
||||
};
|
||||
|
||||
const subscriberOptionsMeetingStarted = {
|
||||
@@ -818,6 +819,7 @@ async function handler(
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_STARTED,
|
||||
teamId,
|
||||
orgId,
|
||||
oAuthClientId: platformClientId,
|
||||
};
|
||||
|
||||
const workflows = await getAllWorkflowsFromEventType(eventType, organizerUser.id);
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
@@ -244,7 +245,6 @@ export const roundRobinManualReassignment = async ({
|
||||
bookingFields: eventType.bookingFields ?? null,
|
||||
booking,
|
||||
}),
|
||||
cancellationReason: "Manually re-assigned",
|
||||
};
|
||||
|
||||
const credentials = await prisma.credential.findMany({
|
||||
@@ -287,7 +287,7 @@ export const roundRobinManualReassignment = async ({
|
||||
},
|
||||
]);
|
||||
|
||||
// Send cancellation email to original organizer
|
||||
// Send cancellation email to previous RR host
|
||||
const cancelledEvt = cloneDeep(evt);
|
||||
cancelledEvt.organizer = {
|
||||
email: originalOrganizer.email,
|
||||
@@ -308,7 +308,8 @@ export const roundRobinManualReassignment = async ({
|
||||
language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
|
||||
},
|
||||
],
|
||||
eventType?.metadata as EventTypeMetadata
|
||||
eventType?.metadata as EventTypeMetadata,
|
||||
{ name: newUser.name, email: newUser.email }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -347,8 +348,9 @@ async function handleWorkflowsUpdate({
|
||||
where: {
|
||||
bookingUid: booking.uid,
|
||||
method: WorkflowMethods.EMAIL,
|
||||
scheduled: true,
|
||||
OR: [{ cancelled: false }, { cancelled: null }],
|
||||
workflowStep: {
|
||||
action: WorkflowActions.EMAIL_HOST,
|
||||
workflow: {
|
||||
trigger: {
|
||||
in: [
|
||||
@@ -365,6 +367,7 @@ async function handleWorkflowsUpdate({
|
||||
referenceId: true,
|
||||
workflowStep: {
|
||||
select: {
|
||||
id: true,
|
||||
template: true,
|
||||
workflow: {
|
||||
select: {
|
||||
@@ -373,6 +376,10 @@ async function handleWorkflowsUpdate({
|
||||
timeUnit: true,
|
||||
},
|
||||
},
|
||||
emailSubject: true,
|
||||
reminderBody: true,
|
||||
sender: true,
|
||||
includeCalendarEvent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -382,10 +389,10 @@ async function handleWorkflowsUpdate({
|
||||
const bookerUrl = await getBookerBaseUrl(orgId);
|
||||
|
||||
for (const workflowReminder of workflowReminders) {
|
||||
const workflowStep = workflowReminder?.workflowStep;
|
||||
const workflow = workflowStep?.workflow;
|
||||
const workflowStep = workflowReminder.workflowStep;
|
||||
|
||||
if (workflowStep && workflow) {
|
||||
if (workflowStep) {
|
||||
const workflow = workflowStep.workflow;
|
||||
await scheduleEmailReminder({
|
||||
evt: {
|
||||
...evt,
|
||||
@@ -401,6 +408,12 @@ async function handleWorkflowsUpdate({
|
||||
},
|
||||
sendTo: newUser.email,
|
||||
template: workflowStep.template,
|
||||
emailSubject: workflowStep.emailSubject || undefined,
|
||||
emailBody: workflowStep.reminderBody || undefined,
|
||||
sender: workflowStep.sender || SENDER_NAME,
|
||||
hideBranding: true,
|
||||
includeCalendarEvent: workflowStep.includeCalendarEvent,
|
||||
workflowStepId: workflowStep.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||
import { SENDER_NAME } from "@calcom/lib/constants";
|
||||
import { getBookerBaseUrl } from "@calcom/lib/getBookerUrl/server";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getLuckyUser } from "@calcom/lib/server";
|
||||
@@ -375,18 +376,20 @@ export const roundRobinReassignment = async ({
|
||||
language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
|
||||
},
|
||||
],
|
||||
eventType?.metadata as EventTypeMetadata
|
||||
eventType?.metadata as EventTypeMetadata,
|
||||
{ name: reassignedRRHost.name, email: reassignedRRHost.email }
|
||||
);
|
||||
}
|
||||
|
||||
// Handle changing workflows with organizer
|
||||
if (hasOrganizerChanged) {
|
||||
const workflowReminders = await prisma.workflowReminder.findMany({
|
||||
const scheduledWorkflowReminders = await prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
bookingUid: booking.uid,
|
||||
method: WorkflowMethods.EMAIL,
|
||||
scheduled: true,
|
||||
OR: [{ cancelled: false }, { cancelled: null }],
|
||||
workflowStep: {
|
||||
action: WorkflowActions.EMAIL_HOST,
|
||||
workflow: {
|
||||
trigger: {
|
||||
in: [
|
||||
@@ -403,6 +406,7 @@ export const roundRobinReassignment = async ({
|
||||
referenceId: true,
|
||||
workflowStep: {
|
||||
select: {
|
||||
id: true,
|
||||
template: true,
|
||||
workflow: {
|
||||
select: {
|
||||
@@ -411,6 +415,10 @@ export const roundRobinReassignment = async ({
|
||||
timeUnit: true,
|
||||
},
|
||||
},
|
||||
emailSubject: true,
|
||||
reminderBody: true,
|
||||
sender: true,
|
||||
includeCalendarEvent: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -420,7 +428,7 @@ export const roundRobinReassignment = async ({
|
||||
|
||||
const bookerUrl = await getBookerBaseUrl(orgId);
|
||||
|
||||
for (const workflowReminder of workflowReminders) {
|
||||
for (const workflowReminder of scheduledWorkflowReminders) {
|
||||
const workflowStep = workflowReminder?.workflowStep;
|
||||
const workflow = workflowStep?.workflow;
|
||||
|
||||
@@ -440,6 +448,12 @@ export const roundRobinReassignment = async ({
|
||||
},
|
||||
sendTo: reassignedRRHost.email,
|
||||
template: workflowStep.template,
|
||||
emailSubject: workflowStep.emailSubject || undefined,
|
||||
emailBody: workflowStep.reminderBody || undefined,
|
||||
sender: workflowStep.sender || SENDER_NAME,
|
||||
hideBranding: true,
|
||||
includeCalendarEvent: workflowStep.includeCalendarEvent,
|
||||
workflowStepId: workflowStep.id,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -420,7 +420,7 @@ const EmailEmbedPreview = ({
|
||||
selectedDateAndTime[key].map((time) => {
|
||||
// If teamId is present on eventType and is not null, it means it is a team event.
|
||||
// So we add 'team/' to the url.
|
||||
const bookingURL = `${WEBSITE_URL}/${
|
||||
const bookingURL = `${eventType.bookerUrl}/${
|
||||
eventType.teamId !== null ? "team/" : ""
|
||||
}${username}/${eventType.slug}?duration=${
|
||||
eventType.length
|
||||
|
||||
@@ -306,8 +306,8 @@ export const Components: Record<FieldType, Component> = {
|
||||
value.push("");
|
||||
setValue(value);
|
||||
}}
|
||||
className="mr-auto">
|
||||
{label}
|
||||
className="mr-auto h-fit whitespace-normal text-left">
|
||||
<span className="flex-1">{label}</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
@@ -18,8 +19,8 @@ export const AverageEventDurationChart = () => {
|
||||
const initialConfigIsReady = !!(initialConfig?.teamId || initialConfig?.userId || initialConfig?.isAll);
|
||||
const { data, isSuccess, isPending } = trpc.viewer.insights.averageEventDuration.useQuery(
|
||||
{
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
startDate: dayjs.utc(startDate).toISOString(),
|
||||
endDate: dayjs.utc(endDate).toISOString(),
|
||||
teamId,
|
||||
eventTypeId: selectedEventTypeId ?? undefined,
|
||||
memberUserId: selectedMemberUserId ?? undefined,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
|
||||
@@ -33,8 +34,8 @@ export const BookingStatusLineChart = () => {
|
||||
} = trpc.viewer.insights.eventsTimeline.useQuery(
|
||||
{
|
||||
timeView: selectedTimeView,
|
||||
startDate: startDate.toISOString(),
|
||||
endDate: endDate.toISOString(),
|
||||
startDate: dayjs.utc(startDate).toISOString(),
|
||||
endDate: dayjs.utc(endDate).toISOString(),
|
||||
teamId: selectedTeamId ?? undefined,
|
||||
eventTypeId: selectedEventTypeId ?? undefined,
|
||||
userId: selectedUserId ?? undefined,
|
||||
|
||||
@@ -39,7 +39,7 @@ export const DateSelect = () => {
|
||||
endDate = dayjs().endOf("day");
|
||||
break;
|
||||
case "w": // Last 7 days
|
||||
startDate = dayjs().subtract(7, "day").startOf("day");
|
||||
startDate = dayjs().subtract(1, "week").startOf("day");
|
||||
endDate = dayjs().endOf("day");
|
||||
break;
|
||||
case "t": // Last 30 days
|
||||
|
||||
@@ -1,13 +1,180 @@
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { readonlyPrisma as prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import type { RawDataInput } from "./raw-data.schema";
|
||||
|
||||
type TimeViewType = "week" | "month" | "year" | "day";
|
||||
|
||||
type DateRange = {
|
||||
startDate: string; // ISO string format
|
||||
endDate: string; // ISO string format
|
||||
formattedDate: string;
|
||||
};
|
||||
|
||||
type StatusAggregate = {
|
||||
completed: number;
|
||||
rescheduled: number;
|
||||
cancelled: number;
|
||||
noShowHost: number;
|
||||
_all: number;
|
||||
uncompleted: number;
|
||||
};
|
||||
|
||||
type AggregateResult = {
|
||||
[date: string]: StatusAggregate;
|
||||
};
|
||||
|
||||
// Recursive function to convert a JSON condition object into SQL
|
||||
// Helper type guard function to check if value has 'in' property
|
||||
function isInCondition(value: any): value is { in: any[] } {
|
||||
return typeof value === "object" && value !== null && "in" in value && Array.isArray(value.in);
|
||||
}
|
||||
|
||||
// Helper type guard function to check if value has 'gte' property
|
||||
function isGteCondition(value: any): value is { gte: any } {
|
||||
return typeof value === "object" && value !== null && "gte" in value;
|
||||
}
|
||||
|
||||
// Helper type guard function to check if value has 'lte' property
|
||||
function isLteCondition(value: any): value is { lte: any } {
|
||||
return typeof value === "object" && value !== null && "lte" in value;
|
||||
}
|
||||
|
||||
function buildSqlCondition(condition: any): string {
|
||||
if (Array.isArray(condition.OR)) {
|
||||
return `(${condition.OR.map(buildSqlCondition).join(" OR ")})`;
|
||||
} else if (Array.isArray(condition.AND)) {
|
||||
return `(${condition.AND.map(buildSqlCondition).join(" AND ")})`;
|
||||
} else {
|
||||
const clauses: string[] = [];
|
||||
for (const [key, value] of Object.entries(condition)) {
|
||||
if (isInCondition(value)) {
|
||||
const valuesList = value.in.map((v) => `'${v}'`).join(", ");
|
||||
clauses.push(`"${key}" IN (${valuesList})`);
|
||||
} else if (isGteCondition(value)) {
|
||||
clauses.push(`"${key}" >= '${value.gte}'`);
|
||||
} else if (isLteCondition(value)) {
|
||||
clauses.push(`"${key}" <= '${value.lte}'`);
|
||||
} else {
|
||||
const formattedValue = typeof value === "string" ? `'${value}'` : value;
|
||||
clauses.push(`"${key}" = ${formattedValue}`);
|
||||
}
|
||||
}
|
||||
return clauses.join(" AND ");
|
||||
}
|
||||
}
|
||||
|
||||
class EventsInsights {
|
||||
static countGroupedByStatusForRanges = async (
|
||||
whereConditional: Prisma.BookingTimeStatusWhereInput,
|
||||
startDate: Dayjs,
|
||||
endDate: Dayjs,
|
||||
timeView: "week" | "month" | "year" | "day"
|
||||
): Promise<AggregateResult> => {
|
||||
// Determine the date truncation and date range based on timeView
|
||||
const formattedStartDate = dayjs(startDate).format("YYYY-MM-DD HH:mm:ss");
|
||||
const formattedEndDate = dayjs(endDate).format("YYYY-MM-DD HH:mm:ss");
|
||||
const whereClause = buildSqlCondition(whereConditional);
|
||||
|
||||
const data = await prisma.$queryRaw<
|
||||
{ periodStart: Date; bookingsCount: number; timeStatus: string; noShowHost: boolean }[]
|
||||
>`
|
||||
SELECT
|
||||
"periodStart",
|
||||
CAST(COUNT(*) AS INTEGER) AS "bookingsCount",
|
||||
"timeStatus",
|
||||
"noShowHost"
|
||||
FROM (
|
||||
SELECT
|
||||
DATE_TRUNC(${timeView}, "createdAt") AS "periodStart",
|
||||
"timeStatus",
|
||||
"noShowHost"
|
||||
FROM
|
||||
"BookingTimeStatus"
|
||||
WHERE
|
||||
"createdAt" BETWEEN ${formattedStartDate}::timestamp AND ${formattedEndDate}::timestamp
|
||||
AND ${Prisma.raw(whereClause)}
|
||||
) AS truncated_dates
|
||||
GROUP BY
|
||||
"periodStart",
|
||||
"timeStatus",
|
||||
"noShowHost"
|
||||
ORDER BY
|
||||
"periodStart";
|
||||
`;
|
||||
|
||||
const aggregate: AggregateResult = {};
|
||||
data.forEach(({ periodStart, bookingsCount, timeStatus, noShowHost }) => {
|
||||
const formattedDate = dayjs(periodStart).format("MMM D, YYYY");
|
||||
|
||||
if (dayjs(periodStart).isAfter(endDate)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the date entry exists in the aggregate object
|
||||
if (!aggregate[formattedDate]) {
|
||||
aggregate[formattedDate] = {
|
||||
completed: 0,
|
||||
rescheduled: 0,
|
||||
cancelled: 0,
|
||||
noShowHost: 0,
|
||||
_all: 0,
|
||||
uncompleted: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Add to the specific status count
|
||||
const statusKey = timeStatus as keyof StatusAggregate;
|
||||
aggregate[formattedDate][statusKey] += Number(bookingsCount);
|
||||
|
||||
// Always add to the total count (_all)
|
||||
aggregate[formattedDate]["_all"] += Number(bookingsCount);
|
||||
|
||||
// Track no-show host counts separately
|
||||
if (noShowHost) {
|
||||
aggregate[formattedDate]["noShowHost"] += Number(bookingsCount);
|
||||
}
|
||||
});
|
||||
|
||||
// Generate a complete list of expected date labels based on the timeline
|
||||
let current = dayjs(startDate);
|
||||
const expectedDates: string[] = [];
|
||||
|
||||
while (current.isBefore(endDate) || current.isSame(endDate)) {
|
||||
const formattedDate = current.format("MMM D, YYYY");
|
||||
expectedDates.push(formattedDate);
|
||||
|
||||
// Increment based on the selected timeView
|
||||
if (timeView === "day") {
|
||||
current = current.add(1, "day");
|
||||
} else if (timeView === "week") {
|
||||
current = current.add(1, "week");
|
||||
} else if (timeView === "month") {
|
||||
current = current.add(1, "month");
|
||||
} else if (timeView === "year") {
|
||||
current = current.add(1, "year");
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in any missing dates with zero counts
|
||||
expectedDates.forEach((label) => {
|
||||
if (!aggregate[label]) {
|
||||
aggregate[label] = {
|
||||
completed: 0,
|
||||
rescheduled: 0,
|
||||
cancelled: 0,
|
||||
noShowHost: 0,
|
||||
_all: 0,
|
||||
uncompleted: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return aggregate;
|
||||
};
|
||||
|
||||
static countGroupedByStatus = async (where: Prisma.BookingTimeStatusWhereInput) => {
|
||||
const data = await prisma.bookingTimeStatus.groupBy({
|
||||
where,
|
||||
@@ -122,24 +289,23 @@ class EventsInsights {
|
||||
}
|
||||
|
||||
static getWeekTimeline(startDate: Dayjs, endDate: Dayjs): string[] {
|
||||
const now = dayjs();
|
||||
const endOfDay = now.endOf("day");
|
||||
let pivotDate = dayjs(startDate);
|
||||
let pivotDate = dayjs(endDate);
|
||||
const dates: string[] = [];
|
||||
|
||||
while (pivotDate.isBefore(endDate) || pivotDate.isSame(endDate)) {
|
||||
const pivotAdded = pivotDate.add(6, "day");
|
||||
const weekEndDate = pivotAdded.isBefore(endOfDay) ? pivotAdded : endOfDay;
|
||||
dates.push(pivotDate.format("YYYY-MM-DD"));
|
||||
// Add the endDate as the last date in the timeline
|
||||
dates.push(pivotDate.format("YYYY-MM-DD"));
|
||||
|
||||
if (pivotDate.isSame(endDate)) {
|
||||
// Move backwards in 6-day increments until reaching or passing the startDate
|
||||
while (pivotDate.isAfter(startDate)) {
|
||||
pivotDate = pivotDate.subtract(7, "day");
|
||||
if (pivotDate.isBefore(startDate)) {
|
||||
break;
|
||||
}
|
||||
|
||||
pivotDate = weekEndDate.add(1, "day");
|
||||
dates.push(pivotDate.format("YYYY-MM-DD"));
|
||||
}
|
||||
|
||||
return dates;
|
||||
// Reverse the array to have the timeline in ascending order
|
||||
return dates.reverse();
|
||||
}
|
||||
|
||||
static getMonthTimeline(startDate: Dayjs, endDate: Dayjs) {
|
||||
@@ -207,7 +373,48 @@ class EventsInsights {
|
||||
where: whereConditional,
|
||||
});
|
||||
|
||||
return csvData;
|
||||
const uids = csvData.filter((b) => b.uid !== null).map((b) => b.uid as string);
|
||||
|
||||
if (uids.length === 0) {
|
||||
return csvData;
|
||||
}
|
||||
|
||||
const bookings = await prisma.booking.findMany({
|
||||
where: {
|
||||
uid: {
|
||||
in: uids,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const bookingMap = new Map(bookings.map((booking) => [booking.uid, booking.attendees[0] || null]));
|
||||
|
||||
return csvData.map((bookingTimeStatus) => {
|
||||
if (!bookingTimeStatus.uid) {
|
||||
// should not be reached because we filtered above
|
||||
return bookingTimeStatus;
|
||||
}
|
||||
|
||||
const booker = bookingMap.get(bookingTimeStatus.uid);
|
||||
|
||||
if (!booker) {
|
||||
return bookingTimeStatus;
|
||||
}
|
||||
|
||||
return {
|
||||
...bookingTimeStatus,
|
||||
bookerEmail: booker.email,
|
||||
bookerName: booker.name,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -288,8 +288,8 @@ export const insightsRouter = router({
|
||||
const baseWhereCondition = {
|
||||
...whereConditional,
|
||||
createdAt: {
|
||||
gte: new Date(startDate),
|
||||
lte: new Date(endDate),
|
||||
gte: dayjs(startDate).startOf("day").toDate(),
|
||||
lte: dayjs(endDate).endOf("day").toDate(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -417,11 +417,11 @@ export const insightsRouter = router({
|
||||
userId: selfUserId,
|
||||
} = input;
|
||||
|
||||
const startDate = dayjs(startDateString);
|
||||
const endDate = dayjs(endDateString);
|
||||
const user = ctx.user;
|
||||
// Convert to UTC without shifting the time zone
|
||||
let startDate = dayjs.utc(startDateString).startOf("day");
|
||||
let endDate = dayjs.utc(endDateString).endOf("day");
|
||||
|
||||
if (selfUserId && user?.id !== selfUserId) {
|
||||
if (selfUserId && ctx.user?.id !== selfUserId) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
@@ -431,12 +431,17 @@ export const insightsRouter = router({
|
||||
|
||||
let timeView = inputTimeView;
|
||||
|
||||
if (timeView === "week") {
|
||||
// Difference between start and end date is less than 14 days use day view
|
||||
if (endDate.diff(startDate, "day") < 14) {
|
||||
timeView = "day";
|
||||
}
|
||||
// Adjust the timeView if the range is less than 14 days
|
||||
if (timeView === "week" && endDate.diff(startDate, "day") < 14) {
|
||||
timeView = "day";
|
||||
}
|
||||
|
||||
// Align startDate to the Monday of the week if timeView is 'week'
|
||||
if (timeView === "week") {
|
||||
startDate = startDate.day(1); // Set startDate to Monday
|
||||
endDate = endDate.day(1).add(6, "day").endOf("day"); // Set endDate to Sunday of the same week
|
||||
}
|
||||
|
||||
const r = await buildBaseWhereCondition({
|
||||
teamId,
|
||||
eventTypeId,
|
||||
@@ -450,53 +455,59 @@ export const insightsRouter = router({
|
||||
},
|
||||
});
|
||||
|
||||
let { whereCondition: whereConditional } = r;
|
||||
const { whereCondition: whereConditional } = r;
|
||||
const timeline = await EventsInsights.getTimeLine(timeView, startDate, endDate);
|
||||
|
||||
// Get timeline data
|
||||
const timeline = await EventsInsights.getTimeLine(timeView, dayjs(startDate), dayjs(endDate));
|
||||
|
||||
// iterate timeline and fetch data
|
||||
if (!timeline) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dateFormat: string = timeView === "year" ? "YYYY" : timeView === "month" ? "MMM YYYY" : "ll";
|
||||
const result = [];
|
||||
|
||||
for (const date of timeline) {
|
||||
// Align date ranges consistently with weeks starting on Monday
|
||||
const dateRanges = timeline.map((date) => {
|
||||
let startOfRange = dayjs.utc(date).startOf(timeView);
|
||||
let endOfRange = dayjs.utc(date).endOf(timeView);
|
||||
|
||||
if (timeView === "week") {
|
||||
startOfRange = dayjs.utc(date).day(1); // Start at Monday in UTC
|
||||
endOfRange = startOfRange.add(6, "day").endOf("day"); // End at Sunday in UTC
|
||||
}
|
||||
|
||||
return {
|
||||
startDate: startOfRange.toISOString(),
|
||||
endDate: endOfRange.toISOString(),
|
||||
formattedDate: startOfRange.format(dateFormat), // Align formatted date for consistency
|
||||
};
|
||||
});
|
||||
// Fetch counts grouped by status for the entire range
|
||||
const countsByStatus = await EventsInsights.countGroupedByStatusForRanges(
|
||||
whereConditional,
|
||||
startDate,
|
||||
endDate,
|
||||
timeView
|
||||
);
|
||||
const result = dateRanges.map(({ formattedDate }) => {
|
||||
const EventData = {
|
||||
Month: dayjs(date).format(dateFormat),
|
||||
Month: formattedDate,
|
||||
Created: 0,
|
||||
Completed: 0,
|
||||
Rescheduled: 0,
|
||||
Cancelled: 0,
|
||||
"No-Show (Host)": 0,
|
||||
};
|
||||
const startOfEndOf = timeView;
|
||||
let startDate = dayjs(date).startOf(startOfEndOf);
|
||||
let endDate = dayjs(date).endOf(startOfEndOf);
|
||||
if (timeView === "week") {
|
||||
startDate = dayjs(date).startOf("day");
|
||||
endDate = dayjs(date).add(6, "day").endOf("day");
|
||||
|
||||
const countsForDateRange = countsByStatus[formattedDate];
|
||||
|
||||
if (countsForDateRange) {
|
||||
EventData["Created"] = countsForDateRange["_all"] || 0;
|
||||
EventData["Completed"] = countsForDateRange["completed"] || 0;
|
||||
EventData["Rescheduled"] = countsForDateRange["rescheduled"] || 0;
|
||||
EventData["Cancelled"] = countsForDateRange["cancelled"] || 0;
|
||||
EventData["No-Show (Host)"] = countsForDateRange["noShowHost"] || 0;
|
||||
}
|
||||
|
||||
whereConditional = {
|
||||
...whereConditional,
|
||||
createdAt: {
|
||||
gte: startDate.toISOString(),
|
||||
lte: endDate.toISOString(),
|
||||
},
|
||||
};
|
||||
|
||||
const countsByStatus = await EventsInsights.countGroupedByStatus(whereConditional);
|
||||
|
||||
EventData["Created"] = countsByStatus["_all"];
|
||||
EventData["Completed"] = countsByStatus["completed"];
|
||||
EventData["Rescheduled"] = countsByStatus["rescheduled"];
|
||||
EventData["Cancelled"] = countsByStatus["cancelled"];
|
||||
EventData["No-Show (Host)"] = countsByStatus["noShowHost"];
|
||||
result.push(EventData);
|
||||
}
|
||||
return EventData;
|
||||
});
|
||||
|
||||
return result;
|
||||
}),
|
||||
@@ -672,8 +683,8 @@ export const insightsRouter = router({
|
||||
return [];
|
||||
}
|
||||
|
||||
const startDate = dayjs(startDateString);
|
||||
const endDate = dayjs(endDateString);
|
||||
const startDate = dayjs.utc(startDateString).startOf("day");
|
||||
const endDate = dayjs.utc(endDateString).endOf("day");
|
||||
|
||||
const { whereCondition: whereConditional } = await buildBaseWhereCondition({
|
||||
teamId,
|
||||
@@ -695,43 +706,42 @@ export const insightsRouter = router({
|
||||
return [];
|
||||
}
|
||||
|
||||
const dateFormat = "ll";
|
||||
const startOfEndOf = timeView === "year" ? "year" : timeView === "month" ? "month" : "week";
|
||||
|
||||
const result = [];
|
||||
const allBookings = await ctx.insightsDb.bookingTimeStatus.findMany({
|
||||
select: {
|
||||
eventLength: true,
|
||||
createdAt: true,
|
||||
},
|
||||
where: {
|
||||
...whereConditional,
|
||||
createdAt: {
|
||||
gte: startDate.toDate(),
|
||||
lte: endDate.toDate(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const resultMap = new Map<string, { totalDuration: number; count: number }>();
|
||||
|
||||
for (const date of timeLine) {
|
||||
const EventData = {
|
||||
Date: dayjs(date).format(dateFormat),
|
||||
Average: 0,
|
||||
};
|
||||
const startOfEndOf = timeView === "year" ? "year" : timeView === "month" ? "month" : "week";
|
||||
|
||||
const startDate = dayjs(date).startOf(startOfEndOf);
|
||||
const endDate = dayjs(date).endOf(startOfEndOf);
|
||||
|
||||
const bookingsInTimeRange = await ctx.insightsDb.bookingTimeStatus.findMany({
|
||||
select: {
|
||||
eventLength: true,
|
||||
},
|
||||
where: {
|
||||
...whereConditional,
|
||||
createdAt: {
|
||||
gte: startDate.toDate(),
|
||||
lte: endDate.toDate(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const avgDuration =
|
||||
bookingsInTimeRange.reduce((acc, booking) => {
|
||||
const duration = booking.eventLength || 0;
|
||||
return acc + duration;
|
||||
}, 0) / bookingsInTimeRange.length;
|
||||
|
||||
EventData["Average"] = Number(avgDuration) || 0;
|
||||
result.push(EventData);
|
||||
resultMap.set(dayjs(date).startOf(startOfEndOf).format("ll"), { totalDuration: 0, count: 0 });
|
||||
}
|
||||
|
||||
for (const booking of allBookings) {
|
||||
const periodStart = dayjs(booking.createdAt).startOf(startOfEndOf).format("ll");
|
||||
if (resultMap.has(periodStart)) {
|
||||
const current = resultMap.get(periodStart)!;
|
||||
current.totalDuration += booking.eventLength || 0;
|
||||
current.count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const result = Array.from(resultMap.entries()).map(([date, { totalDuration, count }]) => ({
|
||||
Date: date,
|
||||
Average: count > 0 ? totalDuration / count : 0,
|
||||
}));
|
||||
|
||||
return result;
|
||||
}),
|
||||
membersWithMostBookings: userBelongsToTeamProcedure
|
||||
@@ -771,7 +781,6 @@ export const insightsRouter = router({
|
||||
|
||||
bookingWhere = {
|
||||
...bookingWhere,
|
||||
teamId,
|
||||
createdAt: {
|
||||
gte: dayjs(startDate).startOf("day").toDate(),
|
||||
lte: dayjs(endDate).endOf("day").toDate(),
|
||||
@@ -863,8 +872,6 @@ export const insightsRouter = router({
|
||||
|
||||
bookingWhere = {
|
||||
...bookingWhere,
|
||||
teamId,
|
||||
eventTypeId,
|
||||
createdAt: {
|
||||
gte: dayjs(startDate).startOf("day").toDate(),
|
||||
lte: dayjs(endDate).endOf("day").toDate(),
|
||||
|
||||
@@ -48,40 +48,46 @@ export const useSchedule = ({
|
||||
const searchParams = useSearchParams();
|
||||
const routedTeamMemberIds = searchParams ? getRoutedTeamMemberIdsFromSearchParams(searchParams) : null;
|
||||
const skipContactOwner = searchParams ? searchParams.get("cal.skipContactOwner") === "true" : false;
|
||||
return trpc.viewer.public.slots.getSchedule.useQuery(
|
||||
{
|
||||
isTeamEvent,
|
||||
usernameList: getUsernameList(username ?? ""),
|
||||
// Prioritize slug over id, since slug is the first value we get available.
|
||||
// If we have a slug, we don't need to fetch the id.
|
||||
// TODO: are queries using eventTypeId faster? Even tho we lost time fetching the id with the slug.
|
||||
...(eventSlug ? { eventTypeSlug: eventSlug } : { eventTypeId: eventId ?? 0 }),
|
||||
// @TODO: Old code fetched 2 days ago if we were fetching the current month.
|
||||
// Do we want / need to keep that behavior?
|
||||
startTime,
|
||||
// if `prefetchNextMonth` is true, two months are fetched at once.
|
||||
endTime,
|
||||
timeZone: timezone!,
|
||||
duration: duration ? `${duration}` : undefined,
|
||||
rescheduleUid,
|
||||
orgSlug,
|
||||
teamMemberEmail,
|
||||
routedTeamMemberIds,
|
||||
skipContactOwner,
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
|
||||
const input = {
|
||||
isTeamEvent,
|
||||
usernameList: getUsernameList(username ?? ""),
|
||||
// Prioritize slug over id, since slug is the first value we get available.
|
||||
// If we have a slug, we don't need to fetch the id.
|
||||
// TODO: are queries using eventTypeId faster? Even tho we lost time fetching the id with the slug.
|
||||
...(eventSlug ? { eventTypeSlug: eventSlug } : { eventTypeId: eventId ?? 0 }),
|
||||
// @TODO: Old code fetched 2 days ago if we were fetching the current month.
|
||||
// Do we want / need to keep that behavior?
|
||||
startTime,
|
||||
// if `prefetchNextMonth` is true, two months are fetched at once.
|
||||
endTime,
|
||||
timeZone: timezone!,
|
||||
duration: duration ? `${duration}` : undefined,
|
||||
rescheduleUid,
|
||||
orgSlug,
|
||||
teamMemberEmail,
|
||||
routedTeamMemberIds,
|
||||
skipContactOwner,
|
||||
};
|
||||
|
||||
const options = {
|
||||
trpc: {
|
||||
context: {
|
||||
skipBatch: true,
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
enabled:
|
||||
Boolean(username) &&
|
||||
Boolean(month) &&
|
||||
Boolean(timezone) &&
|
||||
// Should only wait for one or the other, not both.
|
||||
(Boolean(eventSlug) || Boolean(eventId) || eventId === 0),
|
||||
}
|
||||
);
|
||||
},
|
||||
refetchOnWindowFocus: false,
|
||||
enabled:
|
||||
Boolean(username) &&
|
||||
Boolean(month) &&
|
||||
Boolean(timezone) &&
|
||||
// Should only wait for one or the other, not both.
|
||||
(Boolean(eventSlug) || Boolean(eventId) || eventId === 0),
|
||||
};
|
||||
|
||||
if (isTeamEvent) {
|
||||
return trpc.viewer.highPerf.getTeamSchedule.useQuery(input, options);
|
||||
}
|
||||
|
||||
return trpc.viewer.public.slots.getSchedule.useQuery(input, options);
|
||||
};
|
||||
|
||||
@@ -15,15 +15,20 @@ export class InternalTasker implements Tasker {
|
||||
};
|
||||
async processQueue(): Promise<void> {
|
||||
const tasks = await Task.getNextBatch();
|
||||
console.info(`Processing ${tasks.length} tasks`, tasks);
|
||||
|
||||
const tasksPromises = tasks.map(async (task) => {
|
||||
console.info(`Processing task ${task.id}`, task);
|
||||
const taskHandlerGetter = tasksMap[task.type as keyof typeof tasksMap];
|
||||
if (!taskHandlerGetter) throw new Error(`Task handler not found for type ${task.type}`);
|
||||
|
||||
const taskHandler = await taskHandlerGetter();
|
||||
return taskHandler(task.payload)
|
||||
.then(async () => {
|
||||
await Task.succeed(task.id);
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.info(`Error processing task ${task.id}: ${error}`);
|
||||
await Task.retry(task.id, error instanceof Error ? error.message : "Unknown error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const whereUpcomingTasks: Prisma.TaskWhereInput = {
|
||||
succeededAt: null,
|
||||
// Get only tasks that are scheduled to run now or in the past
|
||||
scheduledAt: {
|
||||
lt: new Date(),
|
||||
lt: new Date().toUTCString(),
|
||||
},
|
||||
// Get only tasks where maxAttemps has not been reached
|
||||
attempts: {
|
||||
@@ -42,6 +42,7 @@ export class Task {
|
||||
options: { scheduledAt?: Date; maxAttempts?: number } = {}
|
||||
) {
|
||||
const { scheduledAt, maxAttempts } = options;
|
||||
console.info("Creating task", { type, payload, scheduledAt, maxAttempts });
|
||||
const newTask = await db.task.create({
|
||||
data: {
|
||||
payload,
|
||||
@@ -54,6 +55,7 @@ export class Task {
|
||||
}
|
||||
|
||||
static async getNextBatch() {
|
||||
console.info("Getting next batch of tasks", whereUpcomingTasks);
|
||||
return db.task.findMany({
|
||||
where: whereUpcomingTasks,
|
||||
orderBy: {
|
||||
@@ -134,15 +136,16 @@ export class Task {
|
||||
}
|
||||
|
||||
static async cleanup() {
|
||||
return db.task.deleteMany({
|
||||
where: {
|
||||
OR: [
|
||||
// Get tasks that have succeeded
|
||||
whereSucceeded,
|
||||
// Get tasks where maxAttemps has been reached
|
||||
whereMaxAttemptsReached,
|
||||
],
|
||||
},
|
||||
});
|
||||
// TODO: Uncomment this later
|
||||
// return db.task.deleteMany({
|
||||
// where: {
|
||||
// OR: [
|
||||
// // Get tasks that have succeeded
|
||||
// whereSucceeded,
|
||||
// // Get tasks where maxAttemps has been reached
|
||||
// whereMaxAttemptsReached,
|
||||
// ],
|
||||
// },
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "@calcom/platform-enums/monorepo";
|
||||
import type {
|
||||
InputBookingField_2024_06_14,
|
||||
Location_2024_06_14,
|
||||
InputLocation_2024_06_14,
|
||||
BookingLimitsCount_2024_06_14,
|
||||
BookingWindow_2024_06_14,
|
||||
BookerLayouts_2024_06_14,
|
||||
@@ -19,9 +19,9 @@ import type {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
SeatOptionsTransformedSchema,
|
||||
SeatOptionsDisabledSchema,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14,
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
|
||||
describe("transformLocationsApiToInternal", () => {
|
||||
it("should transform address", () => {
|
||||
const input: Location_2024_06_14[] = [
|
||||
const input: InputLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "address",
|
||||
address: "London road 10-1",
|
||||
@@ -61,7 +61,7 @@ describe("transformLocationsApiToInternal", () => {
|
||||
});
|
||||
|
||||
it("should transform link", () => {
|
||||
const input: Location_2024_06_14[] = [
|
||||
const input: InputLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "link",
|
||||
link: "https://customvideo.com/join/123456",
|
||||
@@ -79,7 +79,7 @@ describe("transformLocationsApiToInternal", () => {
|
||||
});
|
||||
|
||||
it("should transform integration", () => {
|
||||
const input: Location_2024_06_14[] = [
|
||||
const input: InputLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
@@ -94,7 +94,7 @@ describe("transformLocationsApiToInternal", () => {
|
||||
});
|
||||
|
||||
it("should transform phone", () => {
|
||||
const input: Location_2024_06_14[] = [
|
||||
const input: InputLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "phone",
|
||||
phone: "+37120993151",
|
||||
@@ -112,7 +112,7 @@ describe("transformLocationsApiToInternal", () => {
|
||||
});
|
||||
|
||||
it("should transform attendee address", () => {
|
||||
const input: AttendeeAddressLocation_2024_06_14[] = [
|
||||
const input: InputAttendeeAddressLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "attendeeAddress",
|
||||
},
|
||||
@@ -125,7 +125,7 @@ describe("transformLocationsApiToInternal", () => {
|
||||
});
|
||||
|
||||
it("should transform attendee phone", () => {
|
||||
const input: AttendeePhoneLocation_2024_06_14[] = [
|
||||
const input: InputAttendeePhoneLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "attendeePhone",
|
||||
},
|
||||
@@ -138,7 +138,7 @@ describe("transformLocationsApiToInternal", () => {
|
||||
});
|
||||
|
||||
it("should transform attendee defined", () => {
|
||||
const input: AttendeeDefinedLocation_2024_06_14[] = [
|
||||
const input: InputAttendeeDefinedLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "attendeeDefined",
|
||||
},
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
import { z } from "zod";
|
||||
import type { InputLocation_2024_06_14 } from "@calcom/platform-types";
|
||||
import { type Integration_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
import { type CreateEventTypeInput_2024_06_14, type Integration_2024_06_14 } from "@calcom/platform-types";
|
||||
import type {
|
||||
AttendeeAddressLocation,
|
||||
AttendeeDefinedLocation,
|
||||
AttendeePhoneLocation,
|
||||
OrganizerAddressLocation,
|
||||
OrganizerIntegrationLocation,
|
||||
OrganizerLinkLocation,
|
||||
OrganizerPhoneLocation,
|
||||
} from "../internal/locations";
|
||||
|
||||
const integrationsMapping: Record<Integration_2024_06_14, "integrations:daily"> = {
|
||||
const apiToInternalintegrationsMapping: Record<
|
||||
Integration_2024_06_14,
|
||||
"integrations:daily" | "integrations:google:meet"
|
||||
> = {
|
||||
"cal-video": "integrations:daily",
|
||||
"google-meet": "integrations:google:meet",
|
||||
};
|
||||
|
||||
export function transformLocationsApiToInternal(
|
||||
inputLocations: CreateEventTypeInput_2024_06_14["locations"]
|
||||
) {
|
||||
export function transformLocationsApiToInternal(inputLocations: InputLocation_2024_06_14[] | undefined) {
|
||||
if (!inputLocations) {
|
||||
return [];
|
||||
}
|
||||
@@ -31,7 +42,7 @@ export function transformLocationsApiToInternal(
|
||||
displayLocationPublicly: location.public,
|
||||
} satisfies OrganizerLinkLocation;
|
||||
case "integration":
|
||||
const integrationLabel = integrationsMapping[location.integration];
|
||||
const integrationLabel = apiToInternalintegrationsMapping[location.integration];
|
||||
return { type: integrationLabel } satisfies OrganizerIntegrationLocation;
|
||||
case "phone":
|
||||
return {
|
||||
@@ -44,69 +55,7 @@ export function transformLocationsApiToInternal(
|
||||
case "attendeeDefined":
|
||||
return { type: "somewhereElse" } satisfies AttendeeDefinedLocation;
|
||||
default:
|
||||
throw new Error(`Unsupported location type '${type}'`);
|
||||
throw new Error(`Unsupported input location type '${type}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const integrationsMappingSchema = {
|
||||
"cal-video": z.literal("integrations:daily"),
|
||||
};
|
||||
|
||||
const OrganizerAddressSchema = z.object({
|
||||
type: z.literal("inPerson"),
|
||||
address: z.string(),
|
||||
displayLocationPublicly: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const OrganizerLinkSchema = z.object({
|
||||
type: z.literal("link"),
|
||||
link: z.string().url(),
|
||||
displayLocationPublicly: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const OrganizerIntegrationSchema = z.object({
|
||||
type: z.union([integrationsMappingSchema["cal-video"], integrationsMappingSchema["cal-video"]]),
|
||||
});
|
||||
|
||||
const OrganizerPhoneSchema = z.object({
|
||||
type: z.literal("userPhone"),
|
||||
hostPhoneNumber: z.string(),
|
||||
displayLocationPublicly: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const OrganizerConferencingSchema = z.object({
|
||||
type: z.literal("conferencing"),
|
||||
});
|
||||
|
||||
const AttendeeAddressSchema = z.object({
|
||||
type: z.literal("attendeeInPerson"),
|
||||
});
|
||||
|
||||
const AttendeePhoneSchema = z.object({
|
||||
type: z.literal("phone"),
|
||||
});
|
||||
|
||||
const AttendeeDefinedSchema = z.object({
|
||||
type: z.literal("somewhereElse"),
|
||||
});
|
||||
|
||||
type OrganizerAddressLocation = z.infer<typeof OrganizerAddressSchema>;
|
||||
type OrganizerLinkLocation = z.infer<typeof OrganizerLinkSchema>;
|
||||
export type OrganizerIntegrationLocation = z.infer<typeof OrganizerIntegrationSchema>;
|
||||
type OrganizerPhoneLocation = z.infer<typeof OrganizerPhoneSchema>;
|
||||
type AttendeeAddressLocation = z.infer<typeof AttendeeAddressSchema>;
|
||||
type AttendeePhoneLocation = z.infer<typeof AttendeePhoneSchema>;
|
||||
type AttendeeDefinedLocation = z.infer<typeof AttendeeDefinedSchema>;
|
||||
|
||||
const TransformedLocationSchema = z.union([
|
||||
OrganizerAddressSchema,
|
||||
OrganizerLinkSchema,
|
||||
OrganizerIntegrationSchema,
|
||||
OrganizerPhoneSchema,
|
||||
OrganizerConferencingSchema,
|
||||
AttendeeAddressSchema,
|
||||
AttendeePhoneSchema,
|
||||
AttendeeDefinedSchema,
|
||||
]);
|
||||
export const TransformedLocationsSchema = z.array(TransformedLocationSchema);
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./api-to-internal";
|
||||
export * from "./internal-to-api";
|
||||
export * from "./internal/locations";
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
DefaultFieldOutput_2024_06_14,
|
||||
CustomFieldOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
import type { OutputUnknownBookingField_2024_06_14 } from "@calcom/platform-types/event-types/event-types_2024_06_14/outputs/booking-fields.output";
|
||||
|
||||
export function transformBookingFieldsInternalToApi(
|
||||
databaseBookingFields: (SystemField | CustomField)[]
|
||||
@@ -17,186 +18,201 @@ export function transformBookingFieldsInternalToApi(
|
||||
(field): field is CustomField => field.editable === "user"
|
||||
);
|
||||
|
||||
const responseDefaultFields: DefaultFieldOutput_2024_06_14[] = defaultFields.map((field) => {
|
||||
switch (field.name) {
|
||||
case "name":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "location":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "rescheduleReason":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "title":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "notes":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "guests":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
default:
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
throw new Error(`Unsupported default booking field '${field.name}'.`);
|
||||
}
|
||||
});
|
||||
const responseDefaultFields: (DefaultFieldOutput_2024_06_14 | OutputUnknownBookingField_2024_06_14)[] =
|
||||
defaultFields.map((field) => {
|
||||
switch (field.name) {
|
||||
case "name":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "location":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "rescheduleReason":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "title":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "notes":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "guests":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
case "attendeePhoneNumber":
|
||||
return {
|
||||
isDefault: true,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
required: field.required,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(field),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const responseCustomFields: CustomFieldOutput_2024_06_14[] = customFields.map((field) => {
|
||||
switch (field.type) {
|
||||
case "phone":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "address":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "text":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "number":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "textarea":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiemail":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "boolean":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "select":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
options: field.options ? field.options.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiselect":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "checkbox":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "radio":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unsupported booking field type '${field.type}'.`);
|
||||
}
|
||||
});
|
||||
const responseCustomFields: (CustomFieldOutput_2024_06_14 | OutputUnknownBookingField_2024_06_14)[] =
|
||||
customFields.map((field) => {
|
||||
switch (field.type) {
|
||||
case "phone":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "address":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "text":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "number":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "textarea":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiemail":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "boolean":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "select":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
placeholder: field.placeholder,
|
||||
options: field.options ? field.options.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "multiselect":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "checkbox":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
case "radio":
|
||||
return {
|
||||
isDefault: false,
|
||||
type: field.type,
|
||||
slug: field.name,
|
||||
label: field.label,
|
||||
required: field.required,
|
||||
options: field.options ? field.options?.map((option) => option.value) : [],
|
||||
disableOnPrefill: field.disableOnPrefill,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(field),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return [...responseDefaultFields, ...responseCustomFields];
|
||||
}
|
||||
@@ -316,6 +332,12 @@ const NameSystemFieldSchema = SystemFieldSchema.extend({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const PhoneSystemFieldSchema = SystemFieldSchema.extend({
|
||||
name: z.literal("attendeePhoneNumber"),
|
||||
type: z.literal("phone"),
|
||||
required: z.boolean(),
|
||||
});
|
||||
|
||||
const EmailSystemFieldSchema = SystemFieldSchema.extend({
|
||||
name: z.literal("email"),
|
||||
type: z.literal("email"),
|
||||
@@ -359,10 +381,12 @@ type LocationReasonSystemField = z.infer<typeof LocationReasonSystemFieldSchema>
|
||||
type TitleSystemField = z.infer<typeof TitleSystemFieldSchema>;
|
||||
type NotesSystemField = z.infer<typeof NotesSystemFieldSchema>;
|
||||
type GuestsSystemField = z.infer<typeof GuestsSystemFieldSchema>;
|
||||
type PhoneSystemField = z.infer<typeof PhoneSystemFieldSchema>;
|
||||
|
||||
export type SystemField =
|
||||
| NameSystemField
|
||||
| EmailSystemField
|
||||
| PhoneSystemField
|
||||
| RescheduleReasonSystemField
|
||||
| LocationReasonSystemField
|
||||
| TitleSystemField
|
||||
@@ -374,6 +398,7 @@ export type CustomField = z.infer<typeof CustomFieldsSchema>;
|
||||
const SystemFieldsSchema = z.union([
|
||||
NameSystemFieldSchema,
|
||||
EmailSystemFieldSchema,
|
||||
PhoneSystemFieldSchema,
|
||||
LocationReasonSystemFieldSchema,
|
||||
RescheduleReasonSystemFieldSchema,
|
||||
TitleSystemFieldSchema,
|
||||
@@ -381,7 +406,8 @@ const SystemFieldsSchema = z.union([
|
||||
GuestsSystemFieldSchema,
|
||||
]);
|
||||
|
||||
export const BookingFieldsSchema = z.array(z.union([CustomFieldsSchema, SystemFieldsSchema]));
|
||||
export const BookingFieldSchema = z.union([CustomFieldsSchema, SystemFieldsSchema]);
|
||||
export const BookingFieldsSchema = z.array(BookingFieldSchema);
|
||||
|
||||
export const systemBeforeFieldName: NameSystemField = {
|
||||
type: "name",
|
||||
|
||||
@@ -7,10 +7,6 @@ import {
|
||||
NoticeThresholdUnitEnum,
|
||||
} from "@calcom/platform-enums/monorepo";
|
||||
import type {
|
||||
AddressLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
TransformBookingLimitsSchema_2024_06_14,
|
||||
TransformFutureBookingsLimitSchema_2024_06_14,
|
||||
BookerLayoutsTransformedSchema,
|
||||
@@ -18,9 +14,14 @@ import type {
|
||||
TransformRecurringEventSchema_2024_06_14,
|
||||
SeatOptionsTransformedSchema,
|
||||
SeatOptionsDisabledSchema,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14,
|
||||
OutputAddressLocation_2024_06_14,
|
||||
OutputAttendeeAddressLocation_2024_06_14,
|
||||
OutputAttendeeDefinedLocation_2024_06_14,
|
||||
OutputAttendeePhoneLocation_2024_06_14,
|
||||
OutputIntegrationLocation_2024_06_14,
|
||||
OutputLinkLocation_2024_06_14,
|
||||
OutputPhoneLocation_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import {
|
||||
@@ -51,7 +52,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: AddressLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputAddressLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "address",
|
||||
address: "1234 Main St",
|
||||
@@ -73,7 +74,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: LinkLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputLinkLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "link",
|
||||
link: "https://example.com",
|
||||
@@ -95,7 +96,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: PhoneLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputPhoneLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "phone",
|
||||
phone: "123456789",
|
||||
@@ -115,7 +116,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: IntegrationLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputIntegrationLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
@@ -127,6 +128,91 @@ describe("transformLocationsInternalToApi", () => {
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform integration location", () => {
|
||||
const transformedLocation = [
|
||||
{
|
||||
type: "integrations:discord_video" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: OutputIntegrationLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "discord-video",
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformLocationsInternalToApi(transformedLocation);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform integration location with link and credentialId", () => {
|
||||
const transformedLocation = [
|
||||
{
|
||||
type: "integrations:discord_video" as const,
|
||||
link: "https://discord.com/users/100",
|
||||
credentialId: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: OutputIntegrationLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "discord-video",
|
||||
link: "https://discord.com/users/100",
|
||||
credentialId: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformLocationsInternalToApi(transformedLocation);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform unknown location", () => {
|
||||
const transformedLocation = [
|
||||
{
|
||||
type: "unknown" as const,
|
||||
location: "unknown location",
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: OutputUnknownLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "unknown",
|
||||
location: JSON.stringify(transformedLocation[0]),
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = transformLocationsInternalToApi(transformedLocation);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform unknown integration location", () => {
|
||||
const transformedLocation = [
|
||||
{
|
||||
type: "integrations:unknown_video" as const,
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: OutputUnknownLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "unknown",
|
||||
location: JSON.stringify(transformedLocation[0]),
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = transformLocationsInternalToApi(transformedLocation);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should reverse transform attendee address location", () => {
|
||||
const transformedLocation = [
|
||||
{
|
||||
@@ -134,7 +220,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: AttendeeAddressLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputAttendeeAddressLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "attendeeAddress",
|
||||
},
|
||||
@@ -151,7 +237,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: AttendeePhoneLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputAttendeePhoneLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "attendeePhone",
|
||||
},
|
||||
@@ -168,7 +254,7 @@ describe("transformLocationsInternalToApi", () => {
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput: AttendeeDefinedLocation_2024_06_14[] = [
|
||||
const expectedOutput: OutputAttendeeDefinedLocation_2024_06_14[] = [
|
||||
{
|
||||
type: "attendeeDefined",
|
||||
},
|
||||
@@ -208,11 +294,11 @@ describe("transformBookingFieldsInternalToApi", () => {
|
||||
fullName: {
|
||||
fields: [
|
||||
{
|
||||
name: "fullName",
|
||||
name: "fullName" as const,
|
||||
label: "custom label",
|
||||
placeholder: "custom placeholder",
|
||||
type: "text",
|
||||
required: true,
|
||||
type: "text" as const,
|
||||
required: true as const,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -690,6 +776,73 @@ describe("transformBookingFieldsInternalToApi", () => {
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform system phone field", () => {
|
||||
const transformedField: SystemField[] = [
|
||||
{
|
||||
name: "attendeePhoneNumber",
|
||||
type: "phone",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput = [
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
const result = transformBookingFieldsInternalToApi(transformedField);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
it("should transform unknown field", () => {
|
||||
const transformedField = [
|
||||
{
|
||||
name: "blabla",
|
||||
type: "blabla",
|
||||
hidden: true,
|
||||
sources: [
|
||||
{
|
||||
id: "default",
|
||||
type: "default",
|
||||
label: "Default",
|
||||
},
|
||||
],
|
||||
editable: "system-but-optional",
|
||||
required: false,
|
||||
defaultLabel: "phone_number",
|
||||
},
|
||||
];
|
||||
|
||||
const expectedOutput = [
|
||||
{
|
||||
type: "unknown",
|
||||
slug: "unknown",
|
||||
bookingField: JSON.stringify(transformedField[0]),
|
||||
},
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const result = transformBookingFieldsInternalToApi(transformedField);
|
||||
|
||||
expect(result).toEqual(expectedOutput);
|
||||
});
|
||||
});
|
||||
|
||||
describe("transformIntervalLimitsInternalToApi", () => {
|
||||
|
||||
@@ -1,34 +1,64 @@
|
||||
import type {
|
||||
AddressLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
Integration_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14,
|
||||
OutputConferencingLocation_2024_06_14,
|
||||
OutputIntegration_2024_06_14,
|
||||
OutputIntegrationLocation_2024_06_14,
|
||||
OutputAddressLocation_2024_06_14,
|
||||
OutputAttendeeAddressLocation_2024_06_14,
|
||||
OutputAttendeeDefinedLocation_2024_06_14,
|
||||
OutputAttendeePhoneLocation_2024_06_14,
|
||||
OutputLinkLocation_2024_06_14,
|
||||
OutputPhoneLocation_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
import type { transformLocationsApiToInternal } from "../api-to-internal";
|
||||
import type { InternalLocation } from "../internal/locations";
|
||||
|
||||
const reverseIntegrationsMapping: Record<string, Integration_2024_06_14> = {
|
||||
const internalToApiIntegrationsMapping: Record<string, OutputIntegration_2024_06_14> = {
|
||||
"integrations:daily": "cal-video",
|
||||
"integrations:google:meet": "google-meet",
|
||||
"integrations:zoom": "zoom",
|
||||
"integrations:whereby_video": "whereby-video",
|
||||
"integrations:whatsapp_video": "whatsapp-video",
|
||||
"integrations:webex_video": "webex-video",
|
||||
"integrations:telegram_video": "telegram-video",
|
||||
"integrations:tandem": "tandem",
|
||||
"integrations:sylaps_video": "sylaps-video",
|
||||
"integrations:skype_video": "skype-video",
|
||||
"integrations:sirius_video_video": "sirius-video",
|
||||
"integrations:signal_video": "signal-video",
|
||||
"integrations:shimmer_video": "shimmer-video",
|
||||
"integrations:salesroom_video": "salesroom-video",
|
||||
"integrations:roam_video": "roam-video",
|
||||
"integrations:riverside_video": "riverside-video",
|
||||
"integrations:ping_video": "ping-video",
|
||||
"integrations:office365_video": "office365-video",
|
||||
"integrations:mirotalk_video": "mirotalk-video",
|
||||
"integrations:jitsi": "jitsi",
|
||||
"integrations:jelly_video": "jelly-video",
|
||||
"integrations:jelly_conferencing": "jelly-conferencing",
|
||||
"integrations:huddle01": "huddle",
|
||||
"integrations:facetime_video": "facetime-video",
|
||||
"integrations:element-call_video": "element-call-video",
|
||||
"integrations:eightxeight_video": "eightxeight-video",
|
||||
"integrations:discord_video": "discord-video",
|
||||
"integrations:demodesk_video": "demodesk-video",
|
||||
"integrations:campsite_conferencing": "campsite-conferencing",
|
||||
"integrations:campfire_video": "campfire-video",
|
||||
"integrations:around_video": "around-video",
|
||||
};
|
||||
|
||||
export function transformLocationsInternalToApi(
|
||||
transformedLocations: ReturnType<typeof transformLocationsApiToInternal>
|
||||
) {
|
||||
if (!transformedLocations) {
|
||||
export function transformLocationsInternalToApi(internalLocations: InternalLocation[] | undefined) {
|
||||
if (!internalLocations) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return transformedLocations.map((location) => {
|
||||
return internalLocations.map((location) => {
|
||||
switch (location.type) {
|
||||
case "inPerson": {
|
||||
if (!location.address) {
|
||||
throw new Error("Address location must have an address");
|
||||
}
|
||||
const addressLocation: AddressLocation_2024_06_14 = {
|
||||
const addressLocation: OutputAddressLocation_2024_06_14 = {
|
||||
type: "address",
|
||||
address: location.address,
|
||||
public: location.displayLocationPublicly,
|
||||
@@ -36,7 +66,7 @@ export function transformLocationsInternalToApi(
|
||||
return addressLocation;
|
||||
}
|
||||
case "attendeeInPerson": {
|
||||
const attendeeAddressLocation: AttendeeAddressLocation_2024_06_14 = {
|
||||
const attendeeAddressLocation: OutputAttendeeAddressLocation_2024_06_14 = {
|
||||
type: "attendeeAddress",
|
||||
};
|
||||
return attendeeAddressLocation;
|
||||
@@ -45,7 +75,7 @@ export function transformLocationsInternalToApi(
|
||||
if (!location.link) {
|
||||
throw new Error("Link location must have a link");
|
||||
}
|
||||
const linkLocation: LinkLocation_2024_06_14 = {
|
||||
const linkLocation: OutputLinkLocation_2024_06_14 = {
|
||||
type: "link",
|
||||
link: location.link,
|
||||
public: location.displayLocationPublicly,
|
||||
@@ -56,7 +86,7 @@ export function transformLocationsInternalToApi(
|
||||
if (!location.hostPhoneNumber) {
|
||||
throw new Error("Phone location must have a phone number");
|
||||
}
|
||||
const phoneLocation: PhoneLocation_2024_06_14 = {
|
||||
const phoneLocation: OutputPhoneLocation_2024_06_14 = {
|
||||
type: "phone",
|
||||
phone: location.hostPhoneNumber,
|
||||
public: location.displayLocationPublicly,
|
||||
@@ -64,25 +94,37 @@ export function transformLocationsInternalToApi(
|
||||
return phoneLocation;
|
||||
}
|
||||
case "phone": {
|
||||
const attendeePhoneLocation: AttendeePhoneLocation_2024_06_14 = {
|
||||
const attendeePhoneLocation: OutputAttendeePhoneLocation_2024_06_14 = {
|
||||
type: "attendeePhone",
|
||||
};
|
||||
return attendeePhoneLocation;
|
||||
}
|
||||
case "somewhereElse": {
|
||||
const attendeeDefinedLocation: AttendeeDefinedLocation_2024_06_14 = {
|
||||
const attendeeDefinedLocation: OutputAttendeeDefinedLocation_2024_06_14 = {
|
||||
type: "attendeeDefined",
|
||||
};
|
||||
return attendeeDefinedLocation;
|
||||
}
|
||||
case "conferencing": {
|
||||
const conferencingLocation: OutputConferencingLocation_2024_06_14 = {
|
||||
type: "conferencing",
|
||||
};
|
||||
return conferencingLocation;
|
||||
}
|
||||
default: {
|
||||
const integrationType = reverseIntegrationsMapping[location.type];
|
||||
const integrationType = internalToApiIntegrationsMapping[location.type];
|
||||
if (!integrationType) {
|
||||
throw new Error(`Unsupported integration type '${location.type}'.`);
|
||||
const unknown: OutputUnknownLocation_2024_06_14 = {
|
||||
type: "unknown",
|
||||
location: JSON.stringify(location),
|
||||
};
|
||||
return unknown;
|
||||
}
|
||||
const integration: IntegrationLocation_2024_06_14 = {
|
||||
const integration: OutputIntegrationLocation_2024_06_14 = {
|
||||
type: "integration",
|
||||
integration: integrationType,
|
||||
link: location.link,
|
||||
credentialId: location.credentialId,
|
||||
};
|
||||
return integration;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const integrationsApiAvailable = {
|
||||
"cal-video": z.literal("integrations:daily"),
|
||||
"google-meet": z.literal("integrations:google:meet"),
|
||||
};
|
||||
|
||||
// note(Lauris): these are read only aka they exist in database and are to be returned by a READ operation
|
||||
const integrationsApiUnavailable = {
|
||||
zoom: z.literal("integrations:zoom"),
|
||||
"whereby-video": z.literal("integrations:whereby_video"),
|
||||
"whatsapp-video": z.literal("integrations:whatsapp_video"),
|
||||
"webex-video": z.literal("integrations:webex_video"),
|
||||
"telegram-video": z.literal("integrations:telegram_video"),
|
||||
tandem: z.literal("integrations:tandem"),
|
||||
"sylaps-video": z.literal("integrations:sylaps_video"),
|
||||
"skype-video": z.literal("integrations:skype_video"),
|
||||
"sirius-video": z.literal("integrations:sirius_video_video"),
|
||||
"signal-video": z.literal("integrations:signal_video"),
|
||||
"shimmer-video": z.literal("integrations:shimmer_video"),
|
||||
"salesroom-video": z.literal("integrations:salesroom_video"),
|
||||
"roam-video": z.literal("integrations:roam_video"),
|
||||
"riverside-video": z.literal("integrations:riverside_video"),
|
||||
"ping-video": z.literal("integrations:ping_video"),
|
||||
"office365-video": z.literal("integrations:office365_video"),
|
||||
"mirotalk-video": z.literal("integrations:mirotalk_video"),
|
||||
jitsi: z.literal("integrations:jitsi"),
|
||||
"jelly-video": z.literal("integrations:jelly_video"),
|
||||
"jelly-conferencing": z.literal("integrations:jelly_conferencing"),
|
||||
huddle: z.literal("integrations:huddle01"),
|
||||
"facetime-video": z.literal("integrations:facetime_video"),
|
||||
"element-call-video": z.literal("integrations:element-call_video"),
|
||||
"eightxeight-video": z.literal("integrations:eightxeight_video"),
|
||||
"discord-video": z.literal("integrations:discord_video"),
|
||||
"demodesk-video": z.literal("integrations:demodesk_video"),
|
||||
"campsite-conferencing": z.literal("integrations:campsite_conferencing"),
|
||||
"campfire-video": z.literal("integrations:campfire_video"),
|
||||
"around-video": z.literal("integrations:around_video"),
|
||||
};
|
||||
|
||||
export const integrationsApiToInternalMappingSchema = {
|
||||
...integrationsApiAvailable,
|
||||
...integrationsApiUnavailable,
|
||||
};
|
||||
|
||||
const OrganizerIntegrationSchema = z.object({
|
||||
type: z.union([
|
||||
integrationsApiToInternalMappingSchema["cal-video"],
|
||||
integrationsApiToInternalMappingSchema["google-meet"],
|
||||
integrationsApiToInternalMappingSchema["zoom"],
|
||||
integrationsApiToInternalMappingSchema["whereby-video"],
|
||||
integrationsApiToInternalMappingSchema["whatsapp-video"],
|
||||
integrationsApiToInternalMappingSchema["webex-video"],
|
||||
integrationsApiToInternalMappingSchema["telegram-video"],
|
||||
integrationsApiToInternalMappingSchema["tandem"],
|
||||
integrationsApiToInternalMappingSchema["sylaps-video"],
|
||||
integrationsApiToInternalMappingSchema["skype-video"],
|
||||
integrationsApiToInternalMappingSchema["sirius-video"],
|
||||
integrationsApiToInternalMappingSchema["signal-video"],
|
||||
integrationsApiToInternalMappingSchema["shimmer-video"],
|
||||
integrationsApiToInternalMappingSchema["salesroom-video"],
|
||||
integrationsApiToInternalMappingSchema["roam-video"],
|
||||
integrationsApiToInternalMappingSchema["riverside-video"],
|
||||
integrationsApiToInternalMappingSchema["ping-video"],
|
||||
integrationsApiToInternalMappingSchema["office365-video"],
|
||||
integrationsApiToInternalMappingSchema["mirotalk-video"],
|
||||
integrationsApiToInternalMappingSchema["jitsi"],
|
||||
integrationsApiToInternalMappingSchema["jelly-video"],
|
||||
integrationsApiToInternalMappingSchema["jelly-conferencing"],
|
||||
integrationsApiToInternalMappingSchema["huddle"],
|
||||
integrationsApiToInternalMappingSchema["facetime-video"],
|
||||
integrationsApiToInternalMappingSchema["element-call-video"],
|
||||
integrationsApiToInternalMappingSchema["eightxeight-video"],
|
||||
integrationsApiToInternalMappingSchema["discord-video"],
|
||||
integrationsApiToInternalMappingSchema["demodesk-video"],
|
||||
integrationsApiToInternalMappingSchema["campsite-conferencing"],
|
||||
integrationsApiToInternalMappingSchema["campfire-video"],
|
||||
integrationsApiToInternalMappingSchema["around-video"],
|
||||
]),
|
||||
link: z.string().url().optional(),
|
||||
credentialId: z.number().optional(),
|
||||
});
|
||||
|
||||
const OrganizerAddressSchema = z.object({
|
||||
type: z.literal("inPerson"),
|
||||
address: z.string(),
|
||||
displayLocationPublicly: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const OrganizerLinkSchema = z.object({
|
||||
type: z.literal("link"),
|
||||
link: z.string().url(),
|
||||
displayLocationPublicly: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const OrganizerPhoneSchema = z.object({
|
||||
type: z.literal("userPhone"),
|
||||
hostPhoneNumber: z.string(),
|
||||
displayLocationPublicly: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const OrganizerConferencingSchema = z.object({
|
||||
type: z.literal("conferencing"),
|
||||
});
|
||||
|
||||
const AttendeeAddressSchema = z.object({
|
||||
type: z.literal("attendeeInPerson"),
|
||||
});
|
||||
|
||||
const AttendeePhoneSchema = z.object({
|
||||
type: z.literal("phone"),
|
||||
});
|
||||
|
||||
const AttendeeDefinedSchema = z.object({
|
||||
type: z.literal("somewhereElse"),
|
||||
});
|
||||
|
||||
export type OrganizerAddressLocation = z.infer<typeof OrganizerAddressSchema>;
|
||||
export type OrganizerLinkLocation = z.infer<typeof OrganizerLinkSchema>;
|
||||
export type OrganizerIntegrationLocation = z.infer<typeof OrganizerIntegrationSchema>;
|
||||
export type OrganizerPhoneLocation = z.infer<typeof OrganizerPhoneSchema>;
|
||||
export type OrganizerConferencingSchema = z.infer<typeof OrganizerConferencingSchema>;
|
||||
export type AttendeeAddressLocation = z.infer<typeof AttendeeAddressSchema>;
|
||||
export type AttendeePhoneLocation = z.infer<typeof AttendeePhoneSchema>;
|
||||
export type AttendeeDefinedLocation = z.infer<typeof AttendeeDefinedSchema>;
|
||||
|
||||
export const InternalLocationSchema = z.union([
|
||||
OrganizerAddressSchema,
|
||||
OrganizerLinkSchema,
|
||||
OrganizerIntegrationSchema,
|
||||
OrganizerPhoneSchema,
|
||||
OrganizerConferencingSchema,
|
||||
AttendeeAddressSchema,
|
||||
AttendeePhoneSchema,
|
||||
AttendeeDefinedSchema,
|
||||
]);
|
||||
export type InternalLocation = z.infer<typeof InternalLocationSchema>;
|
||||
|
||||
export const InternalLocationsSchema = z.array(InternalLocationSchema);
|
||||
+16
-31
@@ -1,53 +1,38 @@
|
||||
import type { ITimezoneOption } from "react-timezone-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { ICity } from "@calcom/ui/components/form/timezone-select";
|
||||
|
||||
import { timezones as additionalTimezones } from "../features/additionalTimezones/additionalTimezones";
|
||||
import isProblematicTimezone from "./isProblematicTimezone";
|
||||
|
||||
function findPartialMatch(itemsToSearch: string, searchString: string) {
|
||||
const searchItems = searchString.split(" ");
|
||||
return searchItems.every((i) => itemsToSearch.toLowerCase().indexOf(i.toLowerCase()) >= 0);
|
||||
}
|
||||
export type Timezones = { label: string; timezone: string }[];
|
||||
|
||||
function findFromCity(searchString: string, data: ICity[]): ICity[] {
|
||||
if (searchString) {
|
||||
const cityLookup = data.filter((o) => findPartialMatch(o.city, searchString));
|
||||
return cityLookup?.length ? cityLookup : [];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export const filterByCities = (tz: string, data: ICity[]): ICity[] => {
|
||||
const cityLookup = findFromCity(tz, data);
|
||||
return cityLookup.map(({ city, timezone }) => ({ city, timezone }));
|
||||
const searchTextFilter = (tzOption: Timezones[number], searchText: string) => {
|
||||
return searchText && tzOption.label.toLowerCase().includes(searchText.toLowerCase());
|
||||
};
|
||||
|
||||
export const addCitiesToDropdown = (cities: ICity[]) => {
|
||||
const cityTimezones = cities?.reduce((acc: { [key: string]: string }, city: ICity) => {
|
||||
if (city.timezone !== null && !isProblematicTimezone(city.timezone)) {
|
||||
acc[city.timezone] = city.city;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
export const filterBySearchText = (searchText: string, timezones: Timezones) => {
|
||||
return timezones.filter((tzOption) => searchTextFilter(tzOption, searchText));
|
||||
};
|
||||
|
||||
additionalTimezones.forEach((tz) => {
|
||||
cityTimezones[tz.identifier] = tz.name;
|
||||
});
|
||||
|
||||
return cityTimezones || {};
|
||||
export const addTimezonesToDropdown = (timezones: Timezones) => {
|
||||
return Object.fromEntries(
|
||||
timezones
|
||||
.filter(({ timezone }) => {
|
||||
return timezone !== null && !isProblematicTimezone(timezone);
|
||||
})
|
||||
.map(({ label, timezone }) => [timezone, label])
|
||||
);
|
||||
};
|
||||
|
||||
const formatOffset = (offset: string) =>
|
||||
offset.replace(/^([-+])(0)(\d):00$/, (_, sign, _zero, hour) => `${sign}${hour}:00`);
|
||||
|
||||
export const handleOptionLabel = (option: ITimezoneOption, cities: ICity[]) => {
|
||||
export const handleOptionLabel = (option: ITimezoneOption, timezones: Timezones) => {
|
||||
const offsetUnit = option.label.split("-")[0].substring(1);
|
||||
const cityName = option.label.split(") ")[1];
|
||||
|
||||
const timezoneValue = ` ${offsetUnit} ${formatOffset(dayjs.tz(undefined, option.value).format("Z"))}`;
|
||||
return cities.length > 0
|
||||
return timezones.length > 0
|
||||
? `${cityName}${timezoneValue}`
|
||||
: `${option.value.replace(/_/g, " ")}${timezoneValue}`;
|
||||
};
|
||||
|
||||
+21
-2
@@ -14,6 +14,7 @@ import type {
|
||||
CustomFieldOutput_2024_06_14,
|
||||
EmailDefaultFieldOutput_2024_06_14,
|
||||
EventTypeOutput_2024_06_14,
|
||||
InputLocation_2024_06_14,
|
||||
NameDefaultFieldOutput_2024_06_14,
|
||||
TeamEventTypeOutput_2024_06_14,
|
||||
} from "@calcom/platform-types";
|
||||
@@ -199,7 +200,9 @@ function isDefaultEvent(eventSlug: string) {
|
||||
}
|
||||
|
||||
function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
|
||||
const transformed = transformLocationsApiToInternal(locations);
|
||||
const transformed = transformLocationsApiToInternal(
|
||||
locations.filter((location) => isAtomSupportedLocation(location))
|
||||
);
|
||||
|
||||
const withPrivateHidden = transformed.map((location) => {
|
||||
const { displayLocationPublicly, type } = location;
|
||||
@@ -224,6 +227,22 @@ function getLocations(locations: EventTypeOutput_2024_06_14["locations"]) {
|
||||
return withPrivateHidden;
|
||||
}
|
||||
|
||||
function isAtomSupportedLocation(
|
||||
location: EventTypeOutput_2024_06_14["locations"][number]
|
||||
): location is InputLocation_2024_06_14 {
|
||||
const supportedIntegrations = ["cal-video", "google-meet"];
|
||||
|
||||
return (
|
||||
location.type === "address" ||
|
||||
location.type === "attendeeAddress" ||
|
||||
location.type === "link" ||
|
||||
location.type === "phone" ||
|
||||
location.type === "attendeePhone" ||
|
||||
location.type === "attendeeDefined" ||
|
||||
(location.type === "integration" && supportedIntegrations.includes(location.integration))
|
||||
);
|
||||
}
|
||||
|
||||
function getBookingFields(
|
||||
bookingFields: EventTypeOutput_2024_06_14["bookingFields"],
|
||||
defaultFormValues: BookerPlatformWrapperAtomProps["defaultFormValues"] | undefined
|
||||
@@ -274,7 +293,7 @@ function getBookingFields(
|
||||
function isCustomField(
|
||||
field: EventTypeOutput_2024_06_14["bookingFields"][number]
|
||||
): field is CustomFieldOutput_2024_06_14 {
|
||||
return !field.isDefault;
|
||||
return field.type !== "unknown" && !field.isDefault;
|
||||
}
|
||||
|
||||
function isDefaultEditableField(
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -88,8 +88,8 @@ export type { AppsStatus } from "@calcom/types/Calendar";
|
||||
|
||||
export { MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||
|
||||
export { cityTimezonesHandler } from "@calcom/lib/cityTimezonesHandler";
|
||||
export type { CityTimezones } from "@calcom/lib/cityTimezonesHandler";
|
||||
export { cityTimezonesHandler } from "@calcom/features/cityTimezones/cityTimezonesHandler";
|
||||
export type { CityTimezones } from "@calcom/features/cityTimezones/cityTimezonesHandler";
|
||||
|
||||
export { TRPCError } from "@trpc/server";
|
||||
export type { TUpdateInputSchema } from "@calcom/trpc/server/routers/viewer/availability/schedule/update.schema";
|
||||
@@ -126,8 +126,10 @@ export {
|
||||
transformEventTypeColorsInternalToApi,
|
||||
transformSeatsInternalToApi,
|
||||
// note(Lauris): schemas
|
||||
TransformedLocationsSchema,
|
||||
InternalLocationsSchema,
|
||||
InternalLocationSchema,
|
||||
BookingFieldsSchema,
|
||||
BookingFieldSchema,
|
||||
// note(Lauris): constants
|
||||
systemBeforeFieldName,
|
||||
systemBeforeFieldEmail,
|
||||
@@ -140,6 +142,7 @@ export type {
|
||||
CustomField,
|
||||
NameSystemField,
|
||||
EmailSystemField,
|
||||
InternalLocation,
|
||||
} from "@calcom/lib/event-types/transformers";
|
||||
|
||||
export { parseBookingLimit, parseEventTypeColor } from "@calcom/lib";
|
||||
|
||||
@@ -102,7 +102,7 @@ export class CreateBookingInputPipe implements PipeTransform {
|
||||
private isRecurringBookingInput(
|
||||
value: CreateBookingInput
|
||||
): value is CreateRecurringBookingInput_2024_08_13 {
|
||||
return value.hasOwnProperty("eventTypeId");
|
||||
return value.hasOwnProperty("recurrenceCount");
|
||||
}
|
||||
|
||||
private isInstantBookingInput(value: CreateBookingInput): value is CreateInstantBookingInput_2024_08_13 {
|
||||
|
||||
@@ -145,85 +145,7 @@ export class CreateInstantBookingInput_2024_08_13 extends CreateBookingInput_202
|
||||
instant!: boolean;
|
||||
}
|
||||
|
||||
export class CreateRecurringBookingInput_2024_08_13 {
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "The start time of the booking in ISO 8601 format in UTC timezone.",
|
||||
example: "2024-08-13T09:00:00Z",
|
||||
})
|
||||
@IsDateString()
|
||||
start!: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: Number,
|
||||
description: "The ID of the event type that is booked.",
|
||||
example: 123,
|
||||
})
|
||||
@IsInt()
|
||||
eventTypeId!: number;
|
||||
|
||||
@ApiProperty({
|
||||
type: Attendee,
|
||||
description: "The attendee's details.",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
attendee!: Attendee;
|
||||
|
||||
@ApiProperty({
|
||||
type: [String],
|
||||
description: "An optional list of guest emails attending the event.",
|
||||
example: ["guest1@example.com", "guest2@example.com"],
|
||||
required: false,
|
||||
})
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@IsOptional()
|
||||
guests?: string[];
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description:
|
||||
"Deprecated - use 'location' instead. Meeting URL just for this booking. Displayed in email and calendar event. If not provided then cal video link will be generated.",
|
||||
example: "https://example.com/meeting",
|
||||
required: false,
|
||||
deprecated: true,
|
||||
})
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
meetingUrl?: string;
|
||||
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "Location for this booking. Displayed in email and calendar event.",
|
||||
example: "https://example.com/meeting",
|
||||
required: false,
|
||||
})
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
location?: string;
|
||||
|
||||
// todo(Lauris): expose after refactoring metadata https://app.campsite.co/cal/posts/zysq8w9rwm9c
|
||||
// @ApiProperty({
|
||||
// type: Object,
|
||||
// description: "Optional metadata for the booking.",
|
||||
// example: { key: "value" },
|
||||
// required: false,
|
||||
// })
|
||||
// @IsObject()
|
||||
// @IsOptional()
|
||||
// metadata!: Record<string, unknown>;
|
||||
|
||||
@ApiProperty({
|
||||
type: Object,
|
||||
description: "Booking field responses.",
|
||||
example: { customField: "customValue" },
|
||||
required: false,
|
||||
})
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
bookingFieldsResponses?: Record<string, unknown>;
|
||||
|
||||
export class CreateRecurringBookingInput_2024_08_13 extends CreateBookingInput_2024_08_13 {
|
||||
@ApiPropertyOptional({
|
||||
type: Number,
|
||||
description: `The number of recurrences. If not provided then event type recurrence count will be used. Can't be more than
|
||||
|
||||
+1
-1
@@ -521,7 +521,7 @@ class InputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterf
|
||||
boolean: BooleanFieldInput_2024_06_14,
|
||||
};
|
||||
|
||||
private reservedSystemSlugs = ["location", "rescheduleReason"];
|
||||
private reservedSystemSlugs = ["location", "rescheduleReason", "attendeePhoneNumber"];
|
||||
|
||||
async validate(bookingFields: { type: string; slug: string }[]) {
|
||||
if (!Array.isArray(bookingFields)) {
|
||||
|
||||
+23
-23
@@ -54,16 +54,16 @@ import { DestinationCalendar_2024_06_14 } from "./destination-calendar.input";
|
||||
import { Disabled_2024_06_14 } from "./disabled.input";
|
||||
import { EventTypeColor_2024_06_14 } from "./event-type-color.input";
|
||||
import {
|
||||
AddressLocation_2024_06_14,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
InputAddressLocation_2024_06_14,
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputIntegrationLocation_2024_06_14,
|
||||
InputLinkLocation_2024_06_14,
|
||||
InputPhoneLocation_2024_06_14,
|
||||
ValidateLocations_2024_06_14,
|
||||
} from "./locations.input";
|
||||
import type { Location_2024_06_14 } from "./locations.input";
|
||||
import type { InputLocation_2024_06_14 } from "./locations.input";
|
||||
import { Recurrence_2024_06_14 } from "./recurrence.input";
|
||||
import { Seats_2024_06_14 } from "./seats.input";
|
||||
|
||||
@@ -74,10 +74,10 @@ export const CREATE_EVENT_DESCRIPTION_EXAMPLE =
|
||||
export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
|
||||
|
||||
@ApiExtraModels(
|
||||
AddressLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
InputAddressLocation_2024_06_14,
|
||||
InputLinkLocation_2024_06_14,
|
||||
InputIntegrationLocation_2024_06_14,
|
||||
InputPhoneLocation_2024_06_14,
|
||||
PhoneFieldInput_2024_06_14,
|
||||
AddressFieldInput_2024_06_14,
|
||||
TextFieldInput_2024_06_14,
|
||||
@@ -98,9 +98,9 @@ export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
|
||||
Recurrence_2024_06_14,
|
||||
BaseConfirmationPolicy_2024_06_14,
|
||||
Seats_2024_06_14,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14
|
||||
)
|
||||
export class CreateEventTypeInput_2024_06_14 {
|
||||
@IsInt()
|
||||
@@ -127,18 +127,18 @@ export class CreateEventTypeInput_2024_06_14 {
|
||||
description:
|
||||
"Locations where the event will take place. If not provided, cal video link will be used as the location.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(AddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(LinkLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(IntegrationLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(PhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AttendeeAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AttendeePhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AttendeeDefinedLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputLinkLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputIntegrationLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputPhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAttendeeAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAttendeePhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAttendeeDefinedLocation_2024_06_14) },
|
||||
],
|
||||
type: "array",
|
||||
})
|
||||
@Type(() => Object)
|
||||
locations?: Location_2024_06_14[];
|
||||
locations?: InputLocation_2024_06_14[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateInputBookingFields_2024_06_14()
|
||||
|
||||
+37
-37
@@ -5,7 +5,7 @@ import { IsString, IsUrl, IsIn, IsPhoneNumber, IsBoolean } from "class-validator
|
||||
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
|
||||
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
|
||||
|
||||
const locations = [
|
||||
export const inputLocations = [
|
||||
"address",
|
||||
"link",
|
||||
"integration",
|
||||
@@ -15,8 +15,8 @@ const locations = [
|
||||
"attendeeDefined",
|
||||
] as const;
|
||||
|
||||
export class AddressLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputAddressLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({ example: "address", description: "only allowed value for type is `address`" })
|
||||
type!: "address";
|
||||
|
||||
@@ -29,8 +29,8 @@ export class AddressLocation_2024_06_14 {
|
||||
public!: boolean;
|
||||
}
|
||||
|
||||
export class LinkLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputLinkLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({ example: "link", description: "only allowed value for type is `link`" })
|
||||
type!: "link";
|
||||
|
||||
@@ -43,21 +43,21 @@ export class LinkLocation_2024_06_14 {
|
||||
public!: boolean;
|
||||
}
|
||||
|
||||
const integrations = ["cal-video"] as const;
|
||||
export type Integration_2024_06_14 = (typeof integrations)[number];
|
||||
export const supportedIntegrations = ["cal-video", "google-meet"] as const;
|
||||
export type Integration_2024_06_14 = (typeof supportedIntegrations)[number];
|
||||
|
||||
export class IntegrationLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputIntegrationLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({ example: "integration", description: "only allowed value for type is `integration`" })
|
||||
type!: "integration";
|
||||
|
||||
@IsIn(integrations)
|
||||
@DocsProperty({ example: integrations[0], enum: integrations })
|
||||
@IsIn(supportedIntegrations)
|
||||
@DocsProperty({ example: supportedIntegrations[0], enum: supportedIntegrations })
|
||||
integration!: Integration_2024_06_14;
|
||||
}
|
||||
|
||||
export class PhoneLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputPhoneLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({ example: "phone", description: "only allowed value for type is `phone`" })
|
||||
type!: "phone";
|
||||
|
||||
@@ -70,22 +70,22 @@ export class PhoneLocation_2024_06_14 {
|
||||
public!: boolean;
|
||||
}
|
||||
|
||||
export class AttendeeAddressLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputAttendeeAddressLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({
|
||||
example: "attendeeAddress",
|
||||
description: "only allowed value for type is `attendeeAddress`",
|
||||
})
|
||||
type!: "attendeeAddress";
|
||||
}
|
||||
export class AttendeePhoneLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputAttendeePhoneLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({ example: "attendeePhone", description: "only allowed value for type is `attendeePhone`" })
|
||||
type!: "attendeePhone";
|
||||
}
|
||||
|
||||
export class AttendeeDefinedLocation_2024_06_14 {
|
||||
@IsIn(locations)
|
||||
export class InputAttendeeDefinedLocation_2024_06_14 {
|
||||
@IsIn(inputLocations)
|
||||
@DocsProperty({
|
||||
example: "attendeeDefined",
|
||||
description: "only allowed value for type is `attendeeDefined`",
|
||||
@@ -93,25 +93,25 @@ export class AttendeeDefinedLocation_2024_06_14 {
|
||||
type!: "attendeeDefined";
|
||||
}
|
||||
|
||||
export type Location_2024_06_14 =
|
||||
| AddressLocation_2024_06_14
|
||||
| LinkLocation_2024_06_14
|
||||
| IntegrationLocation_2024_06_14
|
||||
| PhoneLocation_2024_06_14
|
||||
| AttendeeAddressLocation_2024_06_14
|
||||
| AttendeePhoneLocation_2024_06_14
|
||||
| AttendeeDefinedLocation_2024_06_14;
|
||||
export type InputLocation_2024_06_14 =
|
||||
| InputAddressLocation_2024_06_14
|
||||
| InputLinkLocation_2024_06_14
|
||||
| InputIntegrationLocation_2024_06_14
|
||||
| InputPhoneLocation_2024_06_14
|
||||
| InputAttendeeAddressLocation_2024_06_14
|
||||
| InputAttendeePhoneLocation_2024_06_14
|
||||
| InputAttendeeDefinedLocation_2024_06_14;
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
class LocationValidator_2024_06_14 implements ValidatorConstraintInterface {
|
||||
private classTypeMap: { [key: string]: new () => Location_2024_06_14 } = {
|
||||
address: AddressLocation_2024_06_14,
|
||||
link: LinkLocation_2024_06_14,
|
||||
integration: IntegrationLocation_2024_06_14,
|
||||
phone: PhoneLocation_2024_06_14,
|
||||
attendeePhone: AttendeePhoneLocation_2024_06_14,
|
||||
attendeeAddress: AttendeeAddressLocation_2024_06_14,
|
||||
attendeeDefined: AttendeeDefinedLocation_2024_06_14,
|
||||
class InputLocationValidator_2024_06_14 implements ValidatorConstraintInterface {
|
||||
private classTypeMap: { [key: string]: new () => InputLocation_2024_06_14 } = {
|
||||
address: InputAddressLocation_2024_06_14,
|
||||
link: InputLinkLocation_2024_06_14,
|
||||
integration: InputIntegrationLocation_2024_06_14,
|
||||
phone: InputPhoneLocation_2024_06_14,
|
||||
attendeePhone: InputAttendeePhoneLocation_2024_06_14,
|
||||
attendeeAddress: InputAttendeeAddressLocation_2024_06_14,
|
||||
attendeeDefined: InputAttendeeDefinedLocation_2024_06_14,
|
||||
};
|
||||
|
||||
async validate(locations: { type: string }[]) {
|
||||
@@ -160,7 +160,7 @@ export function ValidateLocations_2024_06_14(validationOptions?: ValidationOptio
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: new LocationValidator_2024_06_14(),
|
||||
validator: new InputLocationValidator_2024_06_14(),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
+23
-23
@@ -50,24 +50,24 @@ import { DestinationCalendar_2024_06_14 } from "./destination-calendar.input";
|
||||
import { Disabled_2024_06_14 } from "./disabled.input";
|
||||
import { EventTypeColor_2024_06_14 } from "./event-type-color.input";
|
||||
import {
|
||||
AddressLocation_2024_06_14,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
InputAddressLocation_2024_06_14,
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputIntegrationLocation_2024_06_14,
|
||||
InputLinkLocation_2024_06_14,
|
||||
InputPhoneLocation_2024_06_14,
|
||||
ValidateLocations_2024_06_14,
|
||||
} from "./locations.input";
|
||||
import type { Location_2024_06_14 } from "./locations.input";
|
||||
import type { InputLocation_2024_06_14 } from "./locations.input";
|
||||
import { Recurrence_2024_06_14 } from "./recurrence.input";
|
||||
import { Seats_2024_06_14 } from "./seats.input";
|
||||
|
||||
@ApiExtraModels(
|
||||
AddressLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
InputAddressLocation_2024_06_14,
|
||||
InputLinkLocation_2024_06_14,
|
||||
InputIntegrationLocation_2024_06_14,
|
||||
InputPhoneLocation_2024_06_14,
|
||||
PhoneFieldInput_2024_06_14,
|
||||
AddressFieldInput_2024_06_14,
|
||||
TextFieldInput_2024_06_14,
|
||||
@@ -88,9 +88,9 @@ import { Seats_2024_06_14 } from "./seats.input";
|
||||
Recurrence_2024_06_14,
|
||||
BaseConfirmationPolicy_2024_06_14,
|
||||
Seats_2024_06_14,
|
||||
AttendeeAddressLocation_2024_06_14,
|
||||
AttendeePhoneLocation_2024_06_14,
|
||||
AttendeeDefinedLocation_2024_06_14
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14
|
||||
)
|
||||
export class UpdateEventTypeInput_2024_06_14 {
|
||||
@IsOptional()
|
||||
@@ -120,18 +120,18 @@ export class UpdateEventTypeInput_2024_06_14 {
|
||||
description:
|
||||
"Locations where the event will take place. If not provided, cal video link will be used as the location.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(AddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(LinkLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(IntegrationLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(PhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AttendeeAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AttendeePhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AttendeeDefinedLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputLinkLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputIntegrationLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputPhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAttendeeAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAttendeePhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(InputAttendeeDefinedLocation_2024_06_14) },
|
||||
],
|
||||
type: "array",
|
||||
})
|
||||
@Type(() => Object)
|
||||
locations?: Location_2024_06_14[];
|
||||
locations?: InputLocation_2024_06_14[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateInputBookingFields_2024_06_14()
|
||||
|
||||
+65
-6
@@ -205,6 +205,32 @@ export class GuestsDefaultFieldOutput_2024_06_14 {
|
||||
required!: false;
|
||||
}
|
||||
|
||||
export class PhoneDefaultFieldOutput_2024_06_14 {
|
||||
@IsBoolean()
|
||||
@DocsProperty({
|
||||
description: "This property is always true because it's a default field",
|
||||
example: true,
|
||||
default: true,
|
||||
})
|
||||
isDefault = true;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({
|
||||
default: "attendeePhoneNumber",
|
||||
})
|
||||
slug!: "attendeePhoneNumber";
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({
|
||||
default: "phone",
|
||||
})
|
||||
type!: "phone";
|
||||
|
||||
@IsBoolean()
|
||||
@DocsProperty()
|
||||
required!: boolean;
|
||||
}
|
||||
|
||||
export class PhoneFieldOutput_2024_06_14 extends PhoneFieldInput_2024_06_14 {
|
||||
@IsBoolean()
|
||||
@DocsProperty({
|
||||
@@ -315,6 +341,17 @@ export class BooleanFieldOutput_2024_06_14 extends BooleanFieldInput_2024_06_14
|
||||
isDefault = false;
|
||||
}
|
||||
|
||||
export class OutputUnknownBookingField_2024_06_14 {
|
||||
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
|
||||
type!: "unknown";
|
||||
|
||||
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
|
||||
slug!: "unknown";
|
||||
|
||||
@IsString()
|
||||
bookingField!: string;
|
||||
}
|
||||
|
||||
export type DefaultFieldOutput_2024_06_14 =
|
||||
| NameDefaultFieldOutput_2024_06_14
|
||||
| EmailDefaultFieldOutput_2024_06_14
|
||||
@@ -322,7 +359,8 @@ export type DefaultFieldOutput_2024_06_14 =
|
||||
| RescheduleReasonDefaultFieldOutput_2024_06_14
|
||||
| TitleDefaultFieldOutput_2024_06_14
|
||||
| NotesDefaultFieldOutput_2024_06_14
|
||||
| GuestsDefaultFieldOutput_2024_06_14;
|
||||
| GuestsDefaultFieldOutput_2024_06_14
|
||||
| PhoneDefaultFieldOutput_2024_06_14;
|
||||
|
||||
export type CustomFieldOutput_2024_06_14 =
|
||||
| PhoneFieldOutput_2024_06_14
|
||||
@@ -337,7 +375,10 @@ export type CustomFieldOutput_2024_06_14 =
|
||||
| RadioGroupFieldOutput_2024_06_14
|
||||
| BooleanFieldOutput_2024_06_14;
|
||||
|
||||
export type OutputBookingField_2024_06_14 = DefaultFieldOutput_2024_06_14 | CustomFieldOutput_2024_06_14;
|
||||
export type OutputBookingField_2024_06_14 =
|
||||
| DefaultFieldOutput_2024_06_14
|
||||
| CustomFieldOutput_2024_06_14
|
||||
| OutputUnknownBookingField_2024_06_14;
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInterface {
|
||||
@@ -349,6 +390,7 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
|
||||
title: TitleDefaultFieldOutput_2024_06_14,
|
||||
notes: NotesDefaultFieldOutput_2024_06_14,
|
||||
guests: GuestsDefaultFieldOutput_2024_06_14,
|
||||
attendeePhoneNumber: PhoneDefaultFieldOutput_2024_06_14,
|
||||
};
|
||||
|
||||
private customOutputTypeMap: { [key: string]: new () => CustomFieldOutput_2024_06_14 } = {
|
||||
@@ -390,9 +432,13 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
|
||||
`Duplicate bookingFields slug '${slug}' found. All bookingFields slugs must be unique.`
|
||||
);
|
||||
}
|
||||
slugs.push(slug);
|
||||
if (slug !== "unknown") {
|
||||
slugs.push(slug);
|
||||
}
|
||||
|
||||
if (this.isDefaultField(field)) {
|
||||
if (this.isUnknownField(field)) {
|
||||
await this.validateUnknownField(field);
|
||||
} else if (this.isDefaultField(field)) {
|
||||
await this.validateDefaultField(field);
|
||||
} else {
|
||||
await this.validateCustomField(field);
|
||||
@@ -403,13 +449,26 @@ class OutputBookingFieldValidator_2024_06_14 implements ValidatorConstraintInter
|
||||
}
|
||||
|
||||
isDefaultField(field: OutputBookingField_2024_06_14): field is DefaultFieldOutput_2024_06_14 {
|
||||
return field.isDefault === true;
|
||||
return field.type !== "unknown" && "isDefault" in field && field.isDefault === true;
|
||||
}
|
||||
|
||||
isUnknownField(field: OutputBookingField_2024_06_14): field is OutputUnknownBookingField_2024_06_14 {
|
||||
return field.type === "unknown";
|
||||
}
|
||||
|
||||
async validateUnknownField(field: OutputUnknownBookingField_2024_06_14) {
|
||||
const instance = plainToInstance(OutputUnknownBookingField_2024_06_14, field);
|
||||
const errors = await validate(instance);
|
||||
if (errors.length > 0) {
|
||||
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
|
||||
throw new BadRequestException(`Validation failed for ${field.slug} booking field: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async validateDefaultField(field: DefaultFieldOutput_2024_06_14) {
|
||||
const ClassType = this.defaultOutputNameMap[field.slug];
|
||||
if (!ClassType) {
|
||||
throw new BadRequestException(`Unsupported booking field slgu '${field.slug}'.`);
|
||||
throw new BadRequestException(`Unsupported default booking field slug '${field.slug}'.`);
|
||||
}
|
||||
|
||||
const instance = plainToInstance(ClassType, field);
|
||||
|
||||
+25
-22
@@ -11,11 +11,7 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
|
||||
import type {
|
||||
Location_2024_06_14,
|
||||
BookingWindow_2024_06_14,
|
||||
BookingLimitsDuration_2024_06_14,
|
||||
} from "../inputs";
|
||||
import type { BookingWindow_2024_06_14, BookingLimitsDuration_2024_06_14 } from "../inputs";
|
||||
import {
|
||||
EventTypeColor_2024_06_14,
|
||||
Seats_2024_06_14,
|
||||
@@ -30,13 +26,6 @@ import { BookerLayouts_2024_06_14 } from "../inputs/booker-layouts.input";
|
||||
import type { BookingLimitsCount_2024_06_14 } from "../inputs/booking-limits-count.input";
|
||||
import type { ConfirmationPolicy_2024_06_14 } from "../inputs/confirmation-policy.input";
|
||||
import { DestinationCalendar_2024_06_14 } from "../inputs/destination-calendar.input";
|
||||
import {
|
||||
AddressLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
ValidateLocations_2024_06_14,
|
||||
} from "../inputs/locations.input";
|
||||
import {
|
||||
EmailDefaultFieldOutput_2024_06_14,
|
||||
NameDefaultFieldOutput_2024_06_14,
|
||||
@@ -59,6 +48,16 @@ import {
|
||||
} from "../outputs/booking-fields.output";
|
||||
import type { OutputBookingField_2024_06_14 } from "./booking-fields.output";
|
||||
import { ValidateOutputBookingFields_2024_06_14 } from "./booking-fields.output";
|
||||
import type { OutputLocation_2024_06_14 } from "./locations.output";
|
||||
import {
|
||||
OutputAddressLocation_2024_06_14,
|
||||
OutputConferencingLocation_2024_06_14,
|
||||
OutputIntegrationLocation_2024_06_14,
|
||||
OutputLinkLocation_2024_06_14,
|
||||
OutputPhoneLocation_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
ValidateOutputLocations_2024_06_14,
|
||||
} from "./locations.output";
|
||||
|
||||
enum SchedulingTypeEnum {
|
||||
ROUND_ROBIN = "ROUND_ROBIN",
|
||||
@@ -95,10 +94,12 @@ class User_2024_06_14 {
|
||||
}
|
||||
|
||||
@ApiExtraModels(
|
||||
AddressLocation_2024_06_14,
|
||||
LinkLocation_2024_06_14,
|
||||
IntegrationLocation_2024_06_14,
|
||||
PhoneLocation_2024_06_14,
|
||||
OutputAddressLocation_2024_06_14,
|
||||
OutputLinkLocation_2024_06_14,
|
||||
OutputIntegrationLocation_2024_06_14,
|
||||
OutputPhoneLocation_2024_06_14,
|
||||
OutputConferencingLocation_2024_06_14,
|
||||
OutputUnknownLocation_2024_06_14,
|
||||
EmailDefaultFieldOutput_2024_06_14,
|
||||
NameDefaultFieldOutput_2024_06_14,
|
||||
LocationDefaultFieldOutput_2024_06_14,
|
||||
@@ -146,18 +147,20 @@ class BaseEventTypeOutput_2024_06_14 {
|
||||
})
|
||||
description!: string;
|
||||
|
||||
@ValidateLocations_2024_06_14()
|
||||
@ValidateOutputLocations_2024_06_14()
|
||||
@DocsProperty({
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(AddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(LinkLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(IntegrationLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(PhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(OutputAddressLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(OutputLinkLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(OutputIntegrationLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(OutputPhoneLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(OutputConferencingLocation_2024_06_14) },
|
||||
{ $ref: getSchemaPath(OutputUnknownLocation_2024_06_14) },
|
||||
],
|
||||
type: "array",
|
||||
})
|
||||
@Type(() => Object)
|
||||
locations!: Location_2024_06_14[];
|
||||
locations!: OutputLocation_2024_06_14[];
|
||||
|
||||
@ValidateOutputBookingFields_2024_06_14()
|
||||
@DocsProperty()
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from "./event-type.output";
|
||||
export * from "./booking-fields.output";
|
||||
export * from "./locations.output";
|
||||
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { IsUrl, IsIn, IsOptional, IsNumber, IsString } from "class-validator";
|
||||
import type { ValidationOptions, ValidatorConstraintInterface } from "class-validator";
|
||||
import { registerDecorator, validate, ValidatorConstraint } from "class-validator";
|
||||
|
||||
import {
|
||||
InputAddressLocation_2024_06_14,
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputLinkLocation_2024_06_14,
|
||||
inputLocations,
|
||||
InputPhoneLocation_2024_06_14,
|
||||
} from "../inputs";
|
||||
|
||||
const outputLocations = [...inputLocations, "conferencing", "unknown"] as const;
|
||||
|
||||
export class OutputAddressLocation_2024_06_14 extends InputAddressLocation_2024_06_14 {}
|
||||
export class OutputLinkLocation_2024_06_14 extends InputLinkLocation_2024_06_14 {}
|
||||
export class OutputPhoneLocation_2024_06_14 extends InputPhoneLocation_2024_06_14 {}
|
||||
export class OutputAttendeeAddressLocation_2024_06_14 extends InputAttendeeAddressLocation_2024_06_14 {}
|
||||
export class OutputAttendeePhoneLocation_2024_06_14 extends InputAttendeePhoneLocation_2024_06_14 {}
|
||||
export class OutputAttendeeDefinedLocation_2024_06_14 extends InputAttendeeDefinedLocation_2024_06_14 {}
|
||||
|
||||
const integrationsValues = [
|
||||
"cal-video",
|
||||
"google-meet",
|
||||
"zoom",
|
||||
"whereby-video",
|
||||
"whatsapp-video",
|
||||
"webex-video",
|
||||
"telegram-video",
|
||||
"tandem",
|
||||
"sylaps-video",
|
||||
"skype-video",
|
||||
"sirius-video",
|
||||
"signal-video",
|
||||
"shimmer-video",
|
||||
"salesroom-video",
|
||||
"roam-video",
|
||||
"riverside-video",
|
||||
"ping-video",
|
||||
"office365-video",
|
||||
"mirotalk-video",
|
||||
"jitsi",
|
||||
"jelly-video",
|
||||
"jelly-conferencing",
|
||||
"huddle",
|
||||
"facetime-video",
|
||||
"element-call-video",
|
||||
"eightxeight-video",
|
||||
"discord-video",
|
||||
"demodesk-video",
|
||||
"campsite-conferencing",
|
||||
"campfire-video",
|
||||
"around-video",
|
||||
] as const;
|
||||
export type OutputIntegration_2024_06_14 = (typeof integrationsValues)[number];
|
||||
|
||||
export class OutputIntegrationLocation_2024_06_14 {
|
||||
@IsIn(outputLocations)
|
||||
@DocsProperty({ example: "integration", description: "only allowed value for type is `integration`" })
|
||||
type!: "integration";
|
||||
|
||||
@IsIn(integrationsValues)
|
||||
@DocsProperty({ example: integrationsValues[0], enum: integrationsValues })
|
||||
integration!: OutputIntegration_2024_06_14;
|
||||
|
||||
@IsUrl()
|
||||
@IsOptional()
|
||||
link?: string;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
credentialId?: number;
|
||||
}
|
||||
|
||||
export class OutputConferencingLocation_2024_06_14 {
|
||||
@IsIn(outputLocations)
|
||||
@DocsProperty({ example: "conferencing", description: "only allowed value for type is `conferencing`" })
|
||||
type!: "conferencing";
|
||||
}
|
||||
|
||||
export class OutputUnknownLocation_2024_06_14 {
|
||||
@IsIn(outputLocations)
|
||||
@DocsProperty({ example: "unknown", description: "only allowed value for type is `unknown`" })
|
||||
type!: "unknown";
|
||||
|
||||
@IsString()
|
||||
location!: string;
|
||||
}
|
||||
|
||||
export type OutputLocation_2024_06_14 =
|
||||
| OutputAddressLocation_2024_06_14
|
||||
| OutputLinkLocation_2024_06_14
|
||||
| OutputIntegrationLocation_2024_06_14
|
||||
| OutputPhoneLocation_2024_06_14
|
||||
| OutputAttendeeAddressLocation_2024_06_14
|
||||
| OutputAttendeePhoneLocation_2024_06_14
|
||||
| OutputAttendeeDefinedLocation_2024_06_14
|
||||
| OutputConferencingLocation_2024_06_14
|
||||
| OutputUnknownLocation_2024_06_14;
|
||||
|
||||
@ValidatorConstraint({ async: true })
|
||||
class OutputLocationValidator_2024_06_14 implements ValidatorConstraintInterface {
|
||||
private classTypeMap: { [key: string]: new () => OutputLocation_2024_06_14 } = {
|
||||
address: OutputAddressLocation_2024_06_14,
|
||||
link: OutputLinkLocation_2024_06_14,
|
||||
integration: OutputIntegrationLocation_2024_06_14,
|
||||
phone: OutputPhoneLocation_2024_06_14,
|
||||
attendeePhone: OutputAttendeePhoneLocation_2024_06_14,
|
||||
attendeeAddress: OutputAttendeeAddressLocation_2024_06_14,
|
||||
attendeeDefined: OutputAttendeeDefinedLocation_2024_06_14,
|
||||
conferencing: OutputConferencingLocation_2024_06_14,
|
||||
unknown: OutputUnknownLocation_2024_06_14,
|
||||
};
|
||||
|
||||
async validate(locations: { type: string }[]) {
|
||||
if (!Array.isArray(locations)) {
|
||||
throw new BadRequestException(`'locations' must be an array.`);
|
||||
}
|
||||
|
||||
if (!locations.length) {
|
||||
throw new BadRequestException(`'locations' must contain at least 1 location.`);
|
||||
}
|
||||
|
||||
for (const location of locations) {
|
||||
const { type } = location;
|
||||
if (!type) {
|
||||
throw new BadRequestException(`Each object in 'locations' must have a 'type' property.`);
|
||||
}
|
||||
|
||||
const ClassType = this.classTypeMap[type];
|
||||
if (!ClassType) {
|
||||
throw new BadRequestException(`Unsupported output type '${type}'.`);
|
||||
}
|
||||
|
||||
const instance = plainToInstance(ClassType, location);
|
||||
const errors = await validate(instance);
|
||||
if (errors.length > 0) {
|
||||
const message = errors.flatMap((error) => Object.values(error.constraints || {})).join(", ");
|
||||
throw new BadRequestException(`Validation failed for ${type} location: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
defaultMessage() {
|
||||
return `Validation failed for one or more location entries.`;
|
||||
}
|
||||
}
|
||||
|
||||
export function ValidateOutputLocations_2024_06_14(validationOptions?: ValidationOptions) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return function (object: any, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "ValidateLocation",
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: new OutputLocationValidator_2024_06_14(),
|
||||
});
|
||||
};
|
||||
}
|
||||
+2
-2
@@ -7,7 +7,7 @@ import type {
|
||||
transformRecurrenceApiToInternal,
|
||||
transformSeatsApiToInternal,
|
||||
transformBookingFieldsApiToInternal,
|
||||
TransformedLocationsSchema,
|
||||
InternalLocationsSchema,
|
||||
} from "@calcom/lib/event-types/transformers";
|
||||
|
||||
import type { CreateEventTypeInput_2024_06_14, ConfirmationPolicyTransformedSchema } from "../inputs";
|
||||
@@ -32,7 +32,7 @@ export type InputEventTransformed_2024_06_14 = Omit<
|
||||
slug: string;
|
||||
eventName?: string;
|
||||
bookingLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
|
||||
locations?: z.infer<typeof TransformedLocationsSchema>;
|
||||
locations?: z.infer<typeof InternalLocationsSchema>;
|
||||
bookingFields?: ReturnType<typeof transformBookingFieldsApiToInternal>;
|
||||
durationLimits?: ReturnType<typeof transformIntervalLimitsApiToInternal>;
|
||||
recurringEvent?: ReturnType<typeof transformRecurrenceApiToInternal>;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user