feat: v2 and atoms attendee defined meeting locations (#17224)

* request: allow saving attendee address, location and custom location

* response: return new event type locations

* atoms: make elsewhere location work

* update swagger

* chore: update and use platform libraries

* fix schema

* republish libraries

* update lock file

* fix: make somewhereElse optional in response location

* fix: tests
This commit is contained in:
Lauris Skraucis
2024-10-22 13:24:16 +00:00
committed by GitHub
parent de80ca2aaa
commit b768cc5b44
15 changed files with 331 additions and 31 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.48",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.51",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
@@ -1,4 +1,4 @@
import { IntegrationLocation } from "@calcom/lib";
import { OrganizerIntegrationLocation } from "@calcom/lib";
type BaseEventType = {
length: number;
@@ -7,7 +7,7 @@ type BaseEventType = {
};
type EventTypeWithLocation = BaseEventType & {
locations: IntegrationLocation[];
locations: OrganizerIntegrationLocation[];
};
const thirtyMinutes: BaseEventType = {
@@ -217,6 +217,15 @@ describe("Event types Endpoints", () => {
type: "integration",
integration: "cal-video",
},
{
type: "attendeePhone",
},
{
type: "attendeeAddress",
},
{
type: "attendeeDefined",
},
],
bookingFields: [
nameBookingField,
@@ -318,6 +327,8 @@ describe("Event types Endpoints", () => {
const expectedBookingFields = [
{ isDefault: true, required: true, slug: "name", ...nameBookingField },
{ isDefault: true, required: true, slug: "email", type: "email" },
// note(Lauris): location booking field is added if multiple locations are passed
{ isDefault: true, required: false, slug: "location", type: "radioInput" },
{ isDefault: true, required: false, slug: "rescheduleReason", type: "textarea" },
...requestBookingFields
.filter((field) => field.type !== "name" && field.type !== "email")
+77 -1
View File
@@ -6626,6 +6626,45 @@
"showAvailabilityCount"
]
},
"AttendeeAddressLocation_2024_06_14": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "attendeeAddress",
"description": "only allowed value for type is `attendeeAddress`"
}
},
"required": [
"type"
]
},
"AttendeePhoneLocation_2024_06_14": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "attendeePhone",
"description": "only allowed value for type is `attendeePhone`"
}
},
"required": [
"type"
]
},
"AttendeeDefinedLocation_2024_06_14": {
"type": "object",
"properties": {
"type": {
"type": "string",
"example": "attendeeDefined",
"description": "only allowed value for type is `attendeeDefined`"
}
},
"required": [
"type"
]
},
"BookerLayouts_2024_06_14": {
"type": "object",
"properties": {
@@ -6725,6 +6764,15 @@
},
{
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
}
]
}
@@ -7036,7 +7084,8 @@
},
"slug": {
"type": "string",
"default": "location"
"default": "location",
"description": "This booking field is returned only if the event type has more than one location. The purpose of this field is to allow the user to select the location where the event will take place."
},
"type": {
"type": "string",
@@ -8199,6 +8248,15 @@
},
{
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
}
]
}
@@ -9669,6 +9727,15 @@
},
{
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
}
]
}
@@ -10396,6 +10463,15 @@
},
{
"$ref": "#/components/schemas/PhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeAddressLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeePhoneLocation_2024_06_14"
},
{
"$ref": "#/components/schemas/AttendeeDefinedLocation_2024_06_14"
}
]
}
@@ -19,6 +19,9 @@ import type {
CreateEventTypeInput_2024_06_14,
SeatOptionsTransformedSchema,
SeatOptionsDisabledSchema,
AttendeeAddressLocation_2024_06_14,
AttendeePhoneLocation_2024_06_14,
AttendeeDefinedLocation_2024_06_14,
} from "@calcom/platform-types";
import {
@@ -107,6 +110,45 @@ describe("transformLocationsApiToInternal", () => {
expect(result).toEqual(expectedOutput);
});
it("should transform attendee address", () => {
const input: AttendeeAddressLocation_2024_06_14[] = [
{
type: "attendeeAddress",
},
];
const expectedOutput = [{ type: "attendeeInPerson" }];
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform attendee phone", () => {
const input: AttendeePhoneLocation_2024_06_14[] = [
{
type: "attendeePhone",
},
];
const expectedOutput = [{ type: "phone" }];
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
it("should transform attendee defined", () => {
const input: AttendeeDefinedLocation_2024_06_14[] = [
{
type: "attendeeDefined",
},
];
const expectedOutput = [{ type: "somewhereElse" }];
const result = transformLocationsApiToInternal(input);
expect(result).toEqual(expectedOutput);
});
});
describe("transformBookingFieldsApiToInternal", () => {
@@ -21,22 +21,28 @@ export function transformLocationsApiToInternal(
type: "inPerson",
address: location.address,
displayLocationPublicly: location.public,
} satisfies InPersonLocation;
} satisfies OrganizerAddressLocation;
case "attendeeAddress":
return { type: "attendeeInPerson" } satisfies AttendeeAddressLocation;
case "link":
return {
type: "link",
link: location.link,
displayLocationPublicly: location.public,
} satisfies LinkLocation;
} satisfies OrganizerLinkLocation;
case "integration":
const integrationLabel = integrationsMapping[location.integration];
return { type: integrationLabel } satisfies IntegrationLocation;
return { type: integrationLabel } satisfies OrganizerIntegrationLocation;
case "phone":
return {
type: "userPhone",
hostPhoneNumber: location.phone,
displayLocationPublicly: location.public,
} satisfies UserPhoneLocation;
} satisfies OrganizerPhoneLocation;
case "attendeePhone":
return { type: "phone" } satisfies AttendeePhoneLocation;
case "attendeeDefined":
return { type: "somewhereElse" } satisfies AttendeeDefinedLocation;
default:
throw new Error(`Unsupported location type '${type}'`);
}
@@ -47,42 +53,60 @@ const integrationsMappingSchema = {
"cal-video": z.literal("integrations:daily"),
};
const InPersonSchema = z.object({
const OrganizerAddressSchema = z.object({
type: z.literal("inPerson"),
address: z.string(),
displayLocationPublicly: z.boolean().default(false),
});
const LinkSchema = z.object({
const OrganizerLinkSchema = z.object({
type: z.literal("link"),
link: z.string().url(),
displayLocationPublicly: z.boolean().default(false),
});
const IntegrationSchema = z.object({
const OrganizerIntegrationSchema = z.object({
type: z.union([integrationsMappingSchema["cal-video"], integrationsMappingSchema["cal-video"]]),
});
const UserPhoneSchema = z.object({
const OrganizerPhoneSchema = z.object({
type: z.literal("userPhone"),
hostPhoneNumber: z.string(),
displayLocationPublicly: z.boolean().default(false),
});
const ConferencingSchema = z.object({
const OrganizerConferencingSchema = z.object({
type: z.literal("conferencing"),
});
type InPersonLocation = z.infer<typeof InPersonSchema>;
type LinkLocation = z.infer<typeof LinkSchema>;
export type IntegrationLocation = z.infer<typeof IntegrationSchema>;
type UserPhoneLocation = z.infer<typeof UserPhoneSchema>;
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([
InPersonSchema,
LinkSchema,
IntegrationSchema,
UserPhoneSchema,
ConferencingSchema,
OrganizerAddressSchema,
OrganizerLinkSchema,
OrganizerIntegrationSchema,
OrganizerPhoneSchema,
OrganizerConferencingSchema,
AttendeeAddressSchema,
AttendeePhoneSchema,
AttendeeDefinedSchema,
]);
export const TransformedLocationsSchema = z.array(TransformedLocationSchema);
@@ -280,6 +280,13 @@ const SystemFieldSchema = z.object({
required: z.boolean(),
placeholder: z.string(),
}),
somewhereElse: z
.object({
type: z.literal("text"),
required: z.boolean(),
placeholder: z.string(),
})
.optional(),
})
.optional(),
disableOnPrefill: z.boolean().optional(),
@@ -439,6 +446,11 @@ export const systemBeforeFieldLocation: LocationReasonSystemField = {
required: true,
placeholder: "",
},
somewhereElse: {
type: "text",
required: true,
placeholder: "",
},
},
sources: [
{
@@ -18,6 +18,9 @@ import type {
TransformRecurringEventSchema_2024_06_14,
SeatOptionsTransformedSchema,
SeatOptionsDisabledSchema,
AttendeeAddressLocation_2024_06_14,
AttendeePhoneLocation_2024_06_14,
AttendeeDefinedLocation_2024_06_14,
} from "@calcom/platform-types";
import {
@@ -123,6 +126,57 @@ describe("transformLocationsInternalToApi", () => {
expect(result).toEqual(expectedOutput);
});
it("should reverse transform attendee address location", () => {
const transformedLocation = [
{
type: "attendeeInPerson" as const,
},
];
const expectedOutput: AttendeeAddressLocation_2024_06_14[] = [
{
type: "attendeeAddress",
},
];
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform attendee phone location", () => {
const transformedLocation = [
{
type: "phone" as const,
},
];
const expectedOutput: AttendeePhoneLocation_2024_06_14[] = [
{
type: "attendeePhone",
},
];
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
it("should reverse transform attendee defined location", () => {
const transformedLocation = [
{
type: "somewhereElse" as const,
},
];
const expectedOutput: AttendeeDefinedLocation_2024_06_14[] = [
{
type: "attendeeDefined",
},
];
const result = transformLocationsInternalToApi(transformedLocation);
expect(result).toEqual(expectedOutput);
});
});
describe("transformBookingFieldsInternalToApi", () => {
@@ -4,6 +4,9 @@ import type {
LinkLocation_2024_06_14,
PhoneLocation_2024_06_14,
Integration_2024_06_14,
AttendeePhoneLocation_2024_06_14,
AttendeeAddressLocation_2024_06_14,
AttendeeDefinedLocation_2024_06_14,
} from "@calcom/platform-types";
import type { transformLocationsApiToInternal } from "../api-to-internal";
@@ -32,6 +35,12 @@ export function transformLocationsInternalToApi(
};
return addressLocation;
}
case "attendeeInPerson": {
const attendeeAddressLocation: AttendeeAddressLocation_2024_06_14 = {
type: "attendeeAddress",
};
return attendeeAddressLocation;
}
case "link": {
if (!location.link) {
throw new Error("Link location must have a link");
@@ -54,6 +63,18 @@ export function transformLocationsInternalToApi(
};
return phoneLocation;
}
case "phone": {
const attendeePhoneLocation: AttendeePhoneLocation_2024_06_14 = {
type: "attendeePhone",
};
return attendeePhoneLocation;
}
case "somewhereElse": {
const attendeeDefinedLocation: AttendeeDefinedLocation_2024_06_14 = {
type: "attendeeDefined",
};
return attendeeDefinedLocation;
}
default: {
const integrationType = reverseIntegrationsMapping[location.type];
if (!integrationType) {
+3
View File
@@ -1,3 +1,6 @@
## 0.0.51
Released to support PR https://github.com/calcom/cal.com/pull/17224 which enabled attendee specified location during booking.
## 0.0.41
Released to support handle cancel booking passing oauth client id to webhooks.
@@ -55,6 +55,9 @@ 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,
@@ -94,7 +97,10 @@ export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
BaseBookingLimitsDuration_2024_06_14,
Recurrence_2024_06_14,
BaseConfirmationPolicy_2024_06_14,
Seats_2024_06_14
Seats_2024_06_14,
AttendeeAddressLocation_2024_06_14,
AttendeePhoneLocation_2024_06_14,
AttendeeDefinedLocation_2024_06_14
)
export class CreateEventTypeInput_2024_06_14 {
@IsInt()
@@ -125,6 +131,9 @@ export class CreateEventTypeInput_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) },
],
type: "array",
})
@@ -5,7 +5,15 @@ 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 = ["address", "link", "integration", "phone"] as const;
const locations = [
"address",
"link",
"integration",
"phone",
"attendeeAddress",
"attendeePhone",
"attendeeDefined",
] as const;
export class AddressLocation_2024_06_14 {
@IsIn(locations)
@@ -62,11 +70,37 @@ export class PhoneLocation_2024_06_14 {
public!: boolean;
}
export class AttendeeAddressLocation_2024_06_14 {
@IsIn(locations)
@DocsProperty({
example: "attendeeAddress",
description: "only allowed value for type is `attendeeAddress`",
})
type!: "attendeeAddress";
}
export class AttendeePhoneLocation_2024_06_14 {
@IsIn(locations)
@DocsProperty({ example: "attendeePhone", description: "only allowed value for type is `attendeePhone`" })
type!: "attendeePhone";
}
export class AttendeeDefinedLocation_2024_06_14 {
@IsIn(locations)
@DocsProperty({
example: "attendeeDefined",
description: "only allowed value for type is `attendeeDefined`",
})
type!: "attendeeDefined";
}
export type Location_2024_06_14 =
| AddressLocation_2024_06_14
| LinkLocation_2024_06_14
| IntegrationLocation_2024_06_14
| PhoneLocation_2024_06_14;
| PhoneLocation_2024_06_14
| AttendeeAddressLocation_2024_06_14
| AttendeePhoneLocation_2024_06_14
| AttendeeDefinedLocation_2024_06_14;
@ValidatorConstraint({ async: true })
class LocationValidator_2024_06_14 implements ValidatorConstraintInterface {
@@ -75,6 +109,9 @@ class LocationValidator_2024_06_14 implements ValidatorConstraintInterface {
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,
};
async validate(locations: { type: string }[]) {
@@ -51,6 +51,9 @@ 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,
@@ -84,7 +87,10 @@ import { Seats_2024_06_14 } from "./seats.input";
BaseBookingLimitsDuration_2024_06_14,
Recurrence_2024_06_14,
BaseConfirmationPolicy_2024_06_14,
Seats_2024_06_14
Seats_2024_06_14,
AttendeeAddressLocation_2024_06_14,
AttendeePhoneLocation_2024_06_14,
AttendeeDefinedLocation_2024_06_14
)
export class UpdateEventTypeInput_2024_06_14 {
@IsOptional()
@@ -118,6 +124,9 @@ export class UpdateEventTypeInput_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) },
],
type: "array",
})
@@ -85,6 +85,8 @@ export class LocationDefaultFieldOutput_2024_06_14 {
@IsString()
@DocsProperty({
default: "location",
description:
"This booking field is returned only if the event type has more than one location. The purpose of this field is to allow the user to select the location where the event will take place.",
})
slug!: "location";
+5 -5
View File
@@ -4125,7 +4125,7 @@ __metadata:
dependencies:
"@calcom/platform-constants": "*"
"@calcom/platform-enums": "*"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.47"
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.51"
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2"
"@calcom/platform-types": "*"
"@calcom/platform-utils": "*"
@@ -5132,14 +5132,14 @@ __metadata:
languageName: node
linkType: hard
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.47":
version: 0.0.47
resolution: "@calcom/platform-libraries@npm:0.0.47"
"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.51":
version: 0.0.51
resolution: "@calcom/platform-libraries@npm:0.0.51"
dependencies:
"@calcom/core": "*"
"@calcom/features": "*"
"@calcom/lib": "*"
checksum: 2a942510e6d1165903dcb6abb37cc1ab7f8ff1cd8cd69512a5ed6affca2f3a1698c3072b453055140e7aa3def1b04bd1fb153485f50fb4ff390621fd823398b9
checksum: f7b97302730da01067e7417da94aa3e1a3e0e967698516edd15d72be1f2d57f40c382f5b70401aa3758a46f3a9a5a4f29e0fc4b7c8565a9cb4a4ff10ed4b5dc2
languageName: node
linkType: hard