Compare commits
4 Commits
main
...
gh-readonl
Author | SHA1 | Date | |
---|---|---|---|
c1f610d9b6 | |||
30469f486f | |||
e36252f677 | |||
cd5dcd4f7d |
|
@ -0,0 +1,21 @@
|
||||||
|
name: "Validate PRs"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- reopened
|
||||||
|
- edited
|
||||||
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
validate-pr:
|
||||||
|
name: Validate PR title
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: amannn/action-semantic-pull-request@v5
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -318,6 +318,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
<DialogClose />
|
<DialogClose />
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isLoading}
|
||||||
|
data-testid="rejection-confirm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
bookingConfirm(false);
|
bookingConfirm(false);
|
||||||
}}>
|
}}>
|
||||||
|
|
|
@ -39,10 +39,10 @@ test.describe("Managed Event Types tests", () => {
|
||||||
await page.click("[data-testid=new-event-type-dropdown]");
|
await page.click("[data-testid=new-event-type-dropdown]");
|
||||||
await page.click("[data-testid=option-team-1]");
|
await page.click("[data-testid=option-team-1]");
|
||||||
// Expecting we can add a managed event type as team owner
|
// Expecting we can add a managed event type as team owner
|
||||||
await expect(page.locator('input[value="MANAGED"]')).toBeVisible();
|
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
|
||||||
|
|
||||||
// Actually creating a managed event type to test things further
|
// Actually creating a managed event type to test things further
|
||||||
await page.click('input[value="MANAGED"]');
|
await page.click('button[value="MANAGED"]');
|
||||||
await page.fill("[name=title]", "managed");
|
await page.fill("[name=title]", "managed");
|
||||||
await page.click("[type=submit]");
|
await page.click("[type=submit]");
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
|
|
||||||
import { test } from "./lib/fixtures";
|
import { test } from "./lib/fixtures";
|
||||||
import { createHttpServer, selectFirstAvailableTimeSlotNextMonth, waitFor } from "./lib/testUtils";
|
import {
|
||||||
|
bookOptinEvent,
|
||||||
|
createHttpServer,
|
||||||
|
selectFirstAvailableTimeSlotNextMonth,
|
||||||
|
waitFor,
|
||||||
|
} from "./lib/testUtils";
|
||||||
|
|
||||||
test.afterEach(({ users }) => users.deleteAll());
|
test.afterEach(({ users }) => users.deleteAll());
|
||||||
|
|
||||||
test("add webhook & test that creating an event triggers a webhook call", async ({
|
test.describe("BOOKING_CREATED", async () => {
|
||||||
|
test("add webhook & test that creating an event triggers a webhook call", async ({
|
||||||
page,
|
page,
|
||||||
users,
|
users,
|
||||||
}, testInfo) => {
|
}, testInfo) => {
|
||||||
const webhookReceiver = createHttpServer();
|
const webhookReceiver = createHttpServer();
|
||||||
const user = await users.create();
|
const user = await users.create();
|
||||||
const [eventType] = user.eventTypes;
|
const [eventType] = user.eventTypes;
|
||||||
|
@ -128,4 +134,254 @@ test("add webhook & test that creating an event triggers a webhook call", async
|
||||||
});
|
});
|
||||||
|
|
||||||
webhookReceiver.close();
|
webhookReceiver.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("BOOKING_REJECTED", async () => {
|
||||||
|
test("can book an event that requires confirmation and then that booking can be rejected by organizer", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const webhookReceiver = createHttpServer();
|
||||||
|
// --- create a user
|
||||||
|
const user = await users.create();
|
||||||
|
|
||||||
|
// --- visit user page
|
||||||
|
await page.goto(`/${user.username}`);
|
||||||
|
|
||||||
|
// --- book the user's event
|
||||||
|
await bookOptinEvent(page);
|
||||||
|
|
||||||
|
// --- login as that user
|
||||||
|
await user.login();
|
||||||
|
|
||||||
|
await page.goto(`/settings/developer/webhooks`);
|
||||||
|
|
||||||
|
// --- add webhook
|
||||||
|
await page.click('[data-testid="new_webhook"]');
|
||||||
|
|
||||||
|
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||||
|
|
||||||
|
await page.fill('[name="secret"]', "secret");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page.click("[type=submit]"),
|
||||||
|
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// page contains the url
|
||||||
|
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
|
||||||
|
|
||||||
|
await page.goto("/bookings/unconfirmed");
|
||||||
|
await page.click('[data-testid="reject"]');
|
||||||
|
await page.click('[data-testid="rejection-confirm"]');
|
||||||
|
await page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm"));
|
||||||
|
|
||||||
|
// --- check that webhook was called
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(webhookReceiver.requestList.length).toBe(1);
|
||||||
|
});
|
||||||
|
const [request] = webhookReceiver.requestList;
|
||||||
|
const body = request.body as any;
|
||||||
|
|
||||||
|
// remove dynamic properties that differs depending on where you run the tests
|
||||||
|
const dynamic = "[redacted/dynamic]";
|
||||||
|
body.createdAt = dynamic;
|
||||||
|
body.payload.startTime = dynamic;
|
||||||
|
body.payload.endTime = dynamic;
|
||||||
|
body.payload.location = dynamic;
|
||||||
|
for (const attendee of body.payload.attendees) {
|
||||||
|
attendee.timeZone = dynamic;
|
||||||
|
attendee.language = dynamic;
|
||||||
|
}
|
||||||
|
body.payload.organizer.id = dynamic;
|
||||||
|
body.payload.organizer.email = dynamic;
|
||||||
|
body.payload.organizer.timeZone = dynamic;
|
||||||
|
body.payload.organizer.language = dynamic;
|
||||||
|
body.payload.uid = dynamic;
|
||||||
|
body.payload.bookingId = dynamic;
|
||||||
|
body.payload.additionalInformation = dynamic;
|
||||||
|
body.payload.requiresConfirmation = dynamic;
|
||||||
|
body.payload.eventTypeId = dynamic;
|
||||||
|
body.payload.videoCallData = dynamic;
|
||||||
|
body.payload.appsStatus = dynamic;
|
||||||
|
// body.payload.metadata.videoCallUrl = dynamic;
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
triggerEvent: "BOOKING_REJECTED",
|
||||||
|
createdAt: "[redacted/dynamic]",
|
||||||
|
payload: {
|
||||||
|
type: "Opt in",
|
||||||
|
title: "Opt in between Nameless and Test Testson",
|
||||||
|
customInputs: {},
|
||||||
|
startTime: "[redacted/dynamic]",
|
||||||
|
endTime: "[redacted/dynamic]",
|
||||||
|
organizer: {
|
||||||
|
id: "[redacted/dynamic]",
|
||||||
|
name: "Unnamed",
|
||||||
|
email: "[redacted/dynamic]",
|
||||||
|
timeZone: "[redacted/dynamic]",
|
||||||
|
language: "[redacted/dynamic]",
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
email: {
|
||||||
|
value: "test@example.com",
|
||||||
|
label: "email",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "Test Testson",
|
||||||
|
label: "name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userFieldsResponses: {},
|
||||||
|
attendees: [
|
||||||
|
{
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test Testson",
|
||||||
|
timeZone: "[redacted/dynamic]",
|
||||||
|
language: "[redacted/dynamic]",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
location: "[redacted/dynamic]",
|
||||||
|
destinationCalendar: null,
|
||||||
|
// hideCalendarNotes: false,
|
||||||
|
requiresConfirmation: "[redacted/dynamic]",
|
||||||
|
eventTypeId: "[redacted/dynamic]",
|
||||||
|
uid: "[redacted/dynamic]",
|
||||||
|
eventTitle: "Opt in",
|
||||||
|
eventDescription: null,
|
||||||
|
price: 0,
|
||||||
|
currency: "usd",
|
||||||
|
length: 30,
|
||||||
|
bookingId: "[redacted/dynamic]",
|
||||||
|
// metadata: { videoCallUrl: "[redacted/dynamic]" },
|
||||||
|
status: "REJECTED",
|
||||||
|
additionalInformation: "[redacted/dynamic]",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
webhookReceiver.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("BOOKING_REQUESTED", async () => {
|
||||||
|
test("can book an event that requires confirmation and get a booking requested event", async ({
|
||||||
|
page,
|
||||||
|
users,
|
||||||
|
}) => {
|
||||||
|
const webhookReceiver = createHttpServer();
|
||||||
|
// --- create a user
|
||||||
|
const user = await users.create();
|
||||||
|
|
||||||
|
// --- login as that user
|
||||||
|
await user.login();
|
||||||
|
|
||||||
|
await page.goto(`/settings/developer/webhooks`);
|
||||||
|
|
||||||
|
// --- add webhook
|
||||||
|
await page.click('[data-testid="new_webhook"]');
|
||||||
|
|
||||||
|
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||||
|
|
||||||
|
await page.fill('[name="secret"]', "secret");
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
page.click("[type=submit]"),
|
||||||
|
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// page contains the url
|
||||||
|
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
|
||||||
|
|
||||||
|
// --- visit user page
|
||||||
|
await page.goto(`/${user.username}`);
|
||||||
|
|
||||||
|
// --- book the user's opt in
|
||||||
|
await bookOptinEvent(page);
|
||||||
|
|
||||||
|
// --- check that webhook was called
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(webhookReceiver.requestList.length).toBe(1);
|
||||||
|
});
|
||||||
|
const [request] = webhookReceiver.requestList;
|
||||||
|
const body = request.body as any;
|
||||||
|
|
||||||
|
// remove dynamic properties that differs depending on where you run the tests
|
||||||
|
const dynamic = "[redacted/dynamic]";
|
||||||
|
body.createdAt = dynamic;
|
||||||
|
body.payload.startTime = dynamic;
|
||||||
|
body.payload.endTime = dynamic;
|
||||||
|
body.payload.location = dynamic;
|
||||||
|
for (const attendee of body.payload.attendees) {
|
||||||
|
attendee.timeZone = dynamic;
|
||||||
|
attendee.language = dynamic;
|
||||||
|
}
|
||||||
|
body.payload.organizer.id = dynamic;
|
||||||
|
body.payload.organizer.email = dynamic;
|
||||||
|
body.payload.organizer.timeZone = dynamic;
|
||||||
|
body.payload.organizer.language = dynamic;
|
||||||
|
body.payload.uid = dynamic;
|
||||||
|
body.payload.bookingId = dynamic;
|
||||||
|
body.payload.additionalInformation = dynamic;
|
||||||
|
body.payload.requiresConfirmation = dynamic;
|
||||||
|
body.payload.eventTypeId = dynamic;
|
||||||
|
body.payload.videoCallData = dynamic;
|
||||||
|
body.payload.appsStatus = dynamic;
|
||||||
|
body.payload.metadata.videoCallUrl = dynamic;
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
triggerEvent: "BOOKING_REQUESTED",
|
||||||
|
createdAt: "[redacted/dynamic]",
|
||||||
|
payload: {
|
||||||
|
type: "Opt in",
|
||||||
|
title: "Opt in between Nameless and Test Testson",
|
||||||
|
customInputs: {},
|
||||||
|
startTime: "[redacted/dynamic]",
|
||||||
|
endTime: "[redacted/dynamic]",
|
||||||
|
organizer: {
|
||||||
|
id: "[redacted/dynamic]",
|
||||||
|
name: "Nameless",
|
||||||
|
email: "[redacted/dynamic]",
|
||||||
|
timeZone: "[redacted/dynamic]",
|
||||||
|
language: "[redacted/dynamic]",
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
email: {
|
||||||
|
value: "test@example.com",
|
||||||
|
label: "email_address",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
value: "Test Testson",
|
||||||
|
label: "your_name",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
userFieldsResponses: {},
|
||||||
|
attendees: [
|
||||||
|
{
|
||||||
|
email: "test@example.com",
|
||||||
|
name: "Test Testson",
|
||||||
|
timeZone: "[redacted/dynamic]",
|
||||||
|
language: "[redacted/dynamic]",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
location: "[redacted/dynamic]",
|
||||||
|
destinationCalendar: null,
|
||||||
|
requiresConfirmation: "[redacted/dynamic]",
|
||||||
|
eventTypeId: "[redacted/dynamic]",
|
||||||
|
uid: "[redacted/dynamic]",
|
||||||
|
eventTitle: "Opt in",
|
||||||
|
eventDescription: null,
|
||||||
|
price: 0,
|
||||||
|
currency: "usd",
|
||||||
|
length: 30,
|
||||||
|
bookingId: "[redacted/dynamic]",
|
||||||
|
status: "PENDING",
|
||||||
|
additionalInformation: "[redacted/dynamic]",
|
||||||
|
metadata: { videoCallUrl: "[redacted/dynamic]" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
webhookReceiver.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -374,6 +374,8 @@
|
||||||
"booking_rescheduled": "Booking Rescheduled",
|
"booking_rescheduled": "Booking Rescheduled",
|
||||||
"recording_ready":"Recording Download Link Ready",
|
"recording_ready":"Recording Download Link Ready",
|
||||||
"booking_created": "Booking Created",
|
"booking_created": "Booking Created",
|
||||||
|
"booking_rejected":"Booking Rejected",
|
||||||
|
"booking_requested":"Booking Requested",
|
||||||
"meeting_ended": "Meeting Ended",
|
"meeting_ended": "Meeting Ended",
|
||||||
"form_submitted": "Form Submitted",
|
"form_submitted": "Form Submitted",
|
||||||
"event_triggers": "Event Triggers",
|
"event_triggers": "Event Triggers",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
The Cal.com Enterprise Edition (EE) license (the “EE License”)
|
The Cal.com Commercial License (EE) license (the “EE License”)
|
||||||
Copyright (c) 2020-present Cal.com, Inc
|
Copyright (c) 2020-present Cal.com, Inc
|
||||||
|
|
||||||
With regard to the Cal.com Software:
|
With regard to the Cal.com Software:
|
||||||
|
@ -8,7 +8,7 @@ used in production, if you (and any entity that you represent) have agreed to,
|
||||||
and are in compliance with, the Cal.com Subscription Terms available
|
and are in compliance with, the Cal.com Subscription Terms available
|
||||||
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
||||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("EE Subscription")
|
and otherwise have a valid Cal.com Commercial License subscription ("EE Subscription")
|
||||||
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
||||||
you are free to modify this Software and publish patches to the Software. You agree
|
you are free to modify this Software and publish patches to the Software. You agree
|
||||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
The Cal.com Enterprise Edition (EE) license (the “EE License”)
|
The Cal.com Commercial License (EE) license (the “EE License”)
|
||||||
Copyright (c) 2020-present Cal.com, Inc
|
Copyright (c) 2020-present Cal.com, Inc
|
||||||
|
|
||||||
With regard to the Cal.com Software:
|
With regard to the Cal.com Software:
|
||||||
|
@ -8,7 +8,7 @@ used in production, if you (and any entity that you represent) have agreed to,
|
||||||
and are in compliance with, the Cal.com Subscription Terms available
|
and are in compliance with, the Cal.com Subscription Terms available
|
||||||
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
||||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("EE Subscription")
|
and otherwise have a valid Cal.com Commercial License subscription ("EE Subscription")
|
||||||
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
||||||
you are free to modify this Software and publish patches to the Software. You agree
|
you are free to modify this Software and publish patches to the Software. You agree
|
||||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
The Cal.com Enterprise Edition (EE) license (the “EE License”)
|
The Cal.com Commercial License (EE) license (the “EE License”)
|
||||||
Copyright (c) 2020-present Cal.com, Inc
|
Copyright (c) 2020-present Cal.com, Inc
|
||||||
|
|
||||||
With regard to the Cal.com Software:
|
With regard to the Cal.com Software:
|
||||||
|
@ -8,7 +8,7 @@ used in production, if you (and any entity that you represent) have agreed to,
|
||||||
and are in compliance with, the Cal.com Subscription Terms available
|
and are in compliance with, the Cal.com Subscription Terms available
|
||||||
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
||||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("EE Subscription")
|
and otherwise have a valid Cal.com Commercial License subscription ("EE Subscription")
|
||||||
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
||||||
you are free to modify this Software and publish patches to the Software. You agree
|
you are free to modify this Software and publish patches to the Software. You agree
|
||||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
The Cal.com Enterprise Edition (EE) license (the “EE License”)
|
The Cal.com Commercial License (EE) license (the “EE License”)
|
||||||
Copyright (c) 2020-present Cal.com, Inc
|
Copyright (c) 2020-present Cal.com, Inc
|
||||||
|
|
||||||
With regard to the Cal.com Software:
|
With regard to the Cal.com Software:
|
||||||
|
@ -8,7 +8,7 @@ used in production, if you (and any entity that you represent) have agreed to,
|
||||||
and are in compliance with, the Cal.com Subscription Terms available
|
and are in compliance with, the Cal.com Subscription Terms available
|
||||||
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
at https://cal.com/terms (the “EE Terms”), or other agreements governing
|
||||||
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
|
||||||
and otherwise have a valid Cal.com Enterprise Edition subscription ("EE Subscription")
|
and otherwise have a valid Cal.com Commercial License subscription ("EE Subscription")
|
||||||
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
|
||||||
you are free to modify this Software and publish patches to the Software. You agree
|
you are free to modify this Software and publish patches to the Software. You agree
|
||||||
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
} from "@calcom/emails";
|
} from "@calcom/emails";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
|
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||||
import {
|
import {
|
||||||
allowDisablingAttendeeConfirmationEmails,
|
allowDisablingAttendeeConfirmationEmails,
|
||||||
allowDisablingHostConfirmationEmails,
|
allowDisablingHostConfirmationEmails,
|
||||||
|
@ -40,6 +41,7 @@ import {
|
||||||
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||||
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
||||||
|
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
|
||||||
|
@ -72,7 +74,6 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||||
import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
|
import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
|
||||||
|
|
||||||
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
|
||||||
import sendPayload from "../../webhooks/lib/sendPayload";
|
|
||||||
import getBookingResponsesSchema from "./getBookingResponsesSchema";
|
import getBookingResponsesSchema from "./getBookingResponsesSchema";
|
||||||
|
|
||||||
const translator = short();
|
const translator = short();
|
||||||
|
@ -2092,16 +2093,44 @@ async function handler(
|
||||||
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
|
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const eventTypeInfo: EventTypeInfo = {
|
||||||
|
eventTitle: eventType.title,
|
||||||
|
eventDescription: eventType.description,
|
||||||
|
requiresConfirmation: requiresConfirmation || null,
|
||||||
|
price: paymentAppData.price,
|
||||||
|
currency: eventType.currency,
|
||||||
|
length: eventType.length,
|
||||||
|
};
|
||||||
|
const webhookData = {
|
||||||
|
...evt,
|
||||||
|
...eventTypeInfo,
|
||||||
|
bookingId: booking?.id,
|
||||||
|
rescheduleUid,
|
||||||
|
rescheduleStartTime: originalRescheduledBooking?.startTime
|
||||||
|
? dayjs(originalRescheduledBooking?.startTime).utc().format()
|
||||||
|
: undefined,
|
||||||
|
rescheduleEndTime: originalRescheduledBooking?.endTime
|
||||||
|
? dayjs(originalRescheduledBooking?.endTime).utc().format()
|
||||||
|
: undefined,
|
||||||
|
metadata: { ...metadata, ...reqBody.metadata },
|
||||||
|
eventTypeId,
|
||||||
|
status: "ACCEPTED",
|
||||||
|
smsReminderNumber: booking?.smsReminderNumber || undefined,
|
||||||
|
};
|
||||||
|
const subscriberOptions: GetSubscriberOptions = {
|
||||||
|
userId: organizerUser.id,
|
||||||
|
eventTypeId,
|
||||||
|
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
|
||||||
|
teamId: eventType.team?.id,
|
||||||
|
};
|
||||||
|
|
||||||
if (isConfirmedByDefault) {
|
if (isConfirmedByDefault) {
|
||||||
const eventTrigger: WebhookTriggerEvents = rescheduleUid
|
const eventTrigger: WebhookTriggerEvents = rescheduleUid
|
||||||
? WebhookTriggerEvents.BOOKING_RESCHEDULED
|
? WebhookTriggerEvents.BOOKING_RESCHEDULED
|
||||||
: WebhookTriggerEvents.BOOKING_CREATED;
|
: WebhookTriggerEvents.BOOKING_CREATED;
|
||||||
const subscriberOptions = {
|
|
||||||
userId: organizerUser.id,
|
subscriberOptions.triggerEvent = eventTrigger;
|
||||||
eventTypeId,
|
|
||||||
triggerEvent: eventTrigger,
|
|
||||||
teamId: eventType.team?.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscriberOptionsMeetingEnded = {
|
const subscriberOptionsMeetingEnded = {
|
||||||
userId: organizerUser.id,
|
userId: organizerUser.id,
|
||||||
|
@ -2125,48 +2154,14 @@ async function handler(
|
||||||
log.error("Error while running scheduledJobs for booking", error);
|
log.error("Error while running scheduledJobs for booking", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
|
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
|
||||||
const subscribers = await getWebhooks(subscriberOptions);
|
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
|
||||||
console.log("evt:", {
|
} else if (eventType.requiresConfirmation) {
|
||||||
...evt,
|
// if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook
|
||||||
metadata: reqBody.metadata,
|
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED;
|
||||||
});
|
subscriberOptions.triggerEvent = eventTrigger;
|
||||||
const bookingId = booking?.id;
|
webhookData.status = "PENDING";
|
||||||
|
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
|
||||||
const eventTypeInfo: EventTypeInfo = {
|
|
||||||
eventTitle: eventType.title,
|
|
||||||
eventDescription: eventType.description,
|
|
||||||
requiresConfirmation: requiresConfirmation || null,
|
|
||||||
price: paymentAppData.price,
|
|
||||||
currency: eventType.currency,
|
|
||||||
length: eventType.length,
|
|
||||||
};
|
|
||||||
|
|
||||||
const promises = subscribers.map((sub) =>
|
|
||||||
sendPayload(sub.secret, eventTrigger, new Date().toISOString(), sub, {
|
|
||||||
...evt,
|
|
||||||
...eventTypeInfo,
|
|
||||||
bookingId,
|
|
||||||
rescheduleUid,
|
|
||||||
rescheduleStartTime: originalRescheduledBooking?.startTime
|
|
||||||
? dayjs(originalRescheduledBooking?.startTime).utc().format()
|
|
||||||
: undefined,
|
|
||||||
rescheduleEndTime: originalRescheduledBooking?.endTime
|
|
||||||
? dayjs(originalRescheduledBooking?.endTime).utc().format()
|
|
||||||
: undefined,
|
|
||||||
metadata: { ...metadata, ...reqBody.metadata },
|
|
||||||
eventTypeId,
|
|
||||||
status: "ACCEPTED",
|
|
||||||
smsReminderNumber: booking?.smsReminderNumber || undefined,
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await Promise.all(promises);
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Error while sending webhook", error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid passing referencesToCreate with id unique constrain values
|
// Avoid passing referencesToCreate with id unique constrain values
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
|
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
|
import type { WebhookDataType } from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
|
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
|
|
||||||
|
export async function handleWebhookTrigger(args: {
|
||||||
|
subscriberOptions: GetSubscriberOptions;
|
||||||
|
eventTrigger: string;
|
||||||
|
webhookData: Omit<WebhookDataType, "createdAt" | "triggerEvent">;
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const subscribers = await getWebhooks(args.subscriberOptions);
|
||||||
|
|
||||||
|
const promises = subscribers.map((sub) =>
|
||||||
|
sendPayload(sub.secret, args.eventTrigger, new Date().toISOString(), sub, args.webhookData).catch(
|
||||||
|
(e) => {
|
||||||
|
console.error(
|
||||||
|
`Error executing webhook for event: ${args.eventTrigger}, URL: ${sub.subscriberUrl}`,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
await Promise.all(promises);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Error while sending webhook", error);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@calcom/ee",
|
"name": "@calcom/ee",
|
||||||
"description": "Cal.com Enterprise Edition features",
|
"description": "Cal.com Commercial License features",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "See license in LICENSE",
|
"license": "See license in LICENSE",
|
||||||
|
|
|
@ -254,6 +254,9 @@ export default function CreateEventTypeDialog({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RadioArea.Group
|
<RadioArea.Group
|
||||||
|
onValueChange={(val: SchedulingType) => {
|
||||||
|
form.setValue("schedulingType", val);
|
||||||
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-1 flex gap-4",
|
"mt-1 flex gap-4",
|
||||||
isAdmin && flags["managed-event-types"] && "flex-col"
|
isAdmin && flags["managed-event-types"] && "flex-col"
|
||||||
|
|
|
@ -32,6 +32,8 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
|
||||||
core: [
|
core: [
|
||||||
{ value: WebhookTriggerEvents.BOOKING_CANCELLED, label: "booking_cancelled" },
|
{ value: WebhookTriggerEvents.BOOKING_CANCELLED, label: "booking_cancelled" },
|
||||||
{ value: WebhookTriggerEvents.BOOKING_CREATED, label: "booking_created" },
|
{ value: WebhookTriggerEvents.BOOKING_CREATED, label: "booking_created" },
|
||||||
|
{ value: WebhookTriggerEvents.BOOKING_REJECTED, label: "booking_rejected"},
|
||||||
|
{ value: WebhookTriggerEvents.BOOKING_REQUESTED, label: "booking_requested"},
|
||||||
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
|
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
|
||||||
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
|
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
|
||||||
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },
|
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },
|
||||||
|
|
|
@ -8,6 +8,8 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
|
||||||
WebhookTriggerEvents.BOOKING_CREATED,
|
WebhookTriggerEvents.BOOKING_CREATED,
|
||||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||||
WebhookTriggerEvents.MEETING_ENDED,
|
WebhookTriggerEvents.MEETING_ENDED,
|
||||||
|
WebhookTriggerEvents.BOOKING_REQUESTED,
|
||||||
|
WebhookTriggerEvents.BOOKING_REJECTED,
|
||||||
WebhookTriggerEvents.RECORDING_READY,
|
WebhookTriggerEvents.RECORDING_READY,
|
||||||
] as const,
|
] as const,
|
||||||
"routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as const,
|
"routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as const,
|
||||||
|
|
|
@ -16,7 +16,7 @@ export type EventTypeInfo = {
|
||||||
length?: number | null;
|
length?: number | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WebhookDataType = CalendarEvent &
|
export type WebhookDataType = CalendarEvent &
|
||||||
EventTypeInfo & {
|
EventTypeInfo & {
|
||||||
metadata?: { [key: string]: string };
|
metadata?: { [key: string]: string };
|
||||||
bookingId?: number;
|
bookingId?: number;
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REQUESTED';
|
||||||
|
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REJECTED';
|
|
@ -495,7 +495,9 @@ enum PaymentOption {
|
||||||
enum WebhookTriggerEvents {
|
enum WebhookTriggerEvents {
|
||||||
BOOKING_CREATED
|
BOOKING_CREATED
|
||||||
BOOKING_RESCHEDULED
|
BOOKING_RESCHEDULED
|
||||||
|
BOOKING_REQUESTED
|
||||||
BOOKING_CANCELLED
|
BOOKING_CANCELLED
|
||||||
|
BOOKING_REJECTED
|
||||||
FORM_SUBMITTED
|
FORM_SUBMITTED
|
||||||
MEETING_ENDED
|
MEETING_ENDED
|
||||||
RECORDING_READY
|
RECORDING_READY
|
||||||
|
|
|
@ -4,10 +4,12 @@ import appStore from "@calcom/app-store";
|
||||||
import { sendDeclinedEmails } from "@calcom/emails";
|
import { sendDeclinedEmails } from "@calcom/emails";
|
||||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
|
||||||
|
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
|
||||||
|
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getTranslation } from "@calcom/lib/server";
|
import { getTranslation } from "@calcom/lib/server";
|
||||||
import { prisma } from "@calcom/prisma";
|
import { prisma } from "@calcom/prisma";
|
||||||
import { BookingStatus, MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
|
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
|
||||||
|
|
||||||
|
@ -304,6 +306,31 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendDeclinedEmails(evt);
|
await sendDeclinedEmails(evt);
|
||||||
|
// send BOOKING_REJECTED webhooks
|
||||||
|
const subscriberOptions = {
|
||||||
|
userId: booking.userId,
|
||||||
|
eventTypeId: booking.eventTypeId,
|
||||||
|
triggerEvent: WebhookTriggerEvents.BOOKING_REJECTED,
|
||||||
|
teamId: booking.eventType?.teamId,
|
||||||
|
};
|
||||||
|
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REJECTED;
|
||||||
|
const eventTypeInfo: EventTypeInfo = {
|
||||||
|
eventTitle: booking.eventType?.title,
|
||||||
|
eventDescription: booking.eventType?.description,
|
||||||
|
requiresConfirmation: booking.eventType?.requiresConfirmation || null,
|
||||||
|
price: booking.eventType?.price,
|
||||||
|
currency: booking.eventType?.currency,
|
||||||
|
length: booking.eventType?.length,
|
||||||
|
};
|
||||||
|
const webhookData = {
|
||||||
|
...evt,
|
||||||
|
...eventTypeInfo,
|
||||||
|
bookingId,
|
||||||
|
eventTypeId: booking.eventType?.id,
|
||||||
|
status: BookingStatus.REJECTED,
|
||||||
|
smsReminderNumber: booking.smsReminderNumber || undefined,
|
||||||
|
};
|
||||||
|
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = "Booking " + confirmed ? "confirmed" : "rejected";
|
const message = "Booking " + confirmed ? "confirmed" : "rejected";
|
||||||
|
|
|
@ -1,59 +1,57 @@
|
||||||
import React from "react";
|
import { useId } from "@radix-ui/react-id";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
|
||||||
type RadioAreaProps = React.InputHTMLAttributes<HTMLInputElement> & { classNames?: { container?: string } };
|
type RadioAreaProps = RadioGroupPrimitive.RadioGroupItemProps & {
|
||||||
|
children: ReactNode;
|
||||||
|
classNames?: { container?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
const RadioArea = ({ children, className, classNames: innerClassNames, ...props }: RadioAreaProps) => {
|
||||||
|
const radioAreaId = useId();
|
||||||
|
const id = props.id ?? radioAreaId;
|
||||||
|
|
||||||
const RadioArea = React.forwardRef<HTMLInputElement, RadioAreaProps>(
|
|
||||||
({ children, className, classNames: innerClassNames, ...props }, ref) => {
|
|
||||||
return (
|
return (
|
||||||
<label className={classNames("relative flex", className)}>
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
className="text-emphasis bg-subtle border-emphasis focus:ring-none peer absolute top-[0.9rem] left-3 align-baseline"
|
|
||||||
type="radio"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-default peer-checked:border-emphasis border-subtle rounded-md border p-4 pt-3 pl-10",
|
"border-subtle [&:has(input:checked)]:border-emphasis relative flex items-start rounded-md border",
|
||||||
innerClassNames?.container
|
className
|
||||||
)}>
|
)}>
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
id={id}
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
"hover:bg-subtle border-default focus:ring-emphasis absolute top-[0.9rem] left-3 mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border focus:ring-2",
|
||||||
|
props.disabled && "opacity-60"
|
||||||
|
)}>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
className={classNames(
|
||||||
|
"after:bg-default dark:after:bg-inverted relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
|
||||||
|
props.disabled ? "after:bg-muted" : "bg-black"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
<label htmlFor={id} className={classNames("text-default p-4 pt-3 pl-10", innerClassNames?.container)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
|
||||||
</label>
|
</label>
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
type MaybeArray<T> = T[] | T;
|
|
||||||
type ChildrenOfType<T extends React.ElementType> = MaybeArray<
|
|
||||||
React.ReactElement<React.ComponentPropsWithoutRef<T>>
|
|
||||||
>;
|
|
||||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
children: ChildrenOfType<typeof RadioArea>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioAreaGroup = ({ children, className, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
|
||||||
const childrenWithProps = React.Children.map(children, (child) => {
|
|
||||||
if (onChange && React.isValidElement(child)) {
|
|
||||||
return React.cloneElement(child, {
|
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange(e.target.value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div className={className} {...passThroughProps}>
|
|
||||||
{childrenWithProps}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
RadioAreaGroup.displayName = "RadioAreaGroup";
|
const RadioAreaGroup = ({
|
||||||
RadioArea.displayName = "RadioArea";
|
children,
|
||||||
|
className,
|
||||||
|
onValueChange,
|
||||||
|
...passThroughProps
|
||||||
|
}: RadioGroupPrimitive.RadioGroupProps) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root className={className} onValueChange={onValueChange} {...passThroughProps}>
|
||||||
|
{children}
|
||||||
|
</RadioGroupPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Item = RadioArea;
|
const Item = RadioArea;
|
||||||
const Group = RadioAreaGroup;
|
const Group = RadioAreaGroup;
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
|
||||||
import React from "react";
|
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { ChevronDown } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { RadioArea, RadioAreaGroup } from "./RadioAreaGroup";
|
|
||||||
|
|
||||||
interface OptionProps
|
|
||||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
|
|
||||||
interface RadioAreaSelectProps<TFieldValues extends FieldValues>
|
|
||||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "form"> {
|
|
||||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
form: UseFormReturn<TFieldValues>;
|
|
||||||
name: FieldPath<TFieldValues>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select = function RadioAreaSelect<TFieldValues extends FieldValues>(
|
|
||||||
props: RadioAreaSelectProps<TFieldValues>
|
|
||||||
) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const {
|
|
||||||
options,
|
|
||||||
form,
|
|
||||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
|
||||||
placeholder = t("select"),
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
|
||||||
options.find((option: OptionProps) => option.value === value)?.label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible className={classNames("w-full", props.className)}>
|
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
className={classNames(
|
|
||||||
"focus:ring-primary-500 border-default bg-default mb-1 block w-full cursor-pointer rounded-sm border border p-2 text-left shadow-sm sm:text-sm",
|
|
||||||
disabled && "bg-emphasis cursor-default focus:ring-0 "
|
|
||||||
)}>
|
|
||||||
{getLabel(props.value) ?? placeholder}
|
|
||||||
<ChevronDown className="text-subtle float-right h-5 w-5" />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<RadioAreaGroup className="space-y-2 text-sm" onChange={props.onChange}>
|
|
||||||
{options.map((option) => (
|
|
||||||
<RadioArea
|
|
||||||
{...form.register(props.name)}
|
|
||||||
{...option}
|
|
||||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}>
|
|
||||||
<strong className="mb-1 block">{option.label}</strong>
|
|
||||||
<p>{option.description}</p>
|
|
||||||
</RadioArea>
|
|
||||||
))}
|
|
||||||
</RadioAreaGroup>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Select;
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * as RadioGroup from "./RadioAreaGroup";
|
export * as RadioGroup from "./RadioAreaGroup";
|
||||||
export { default as Select } from "./Select";
|
|
||||||
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";
|
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";
|
||||||
|
|
Loading…
Reference in New Issue
Block a user