Merge branch 'main' into feat/organizations
This commit is contained in:
commit
4eec43dbb5
|
@ -57,12 +57,15 @@ const LocationInput = (props: {
|
|||
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||
);
|
||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name={eventLocationType.variable}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field: { onChange, value } }) => {
|
||||
return <PhoneInput onChange={onChange} value={value} {...remainingProps} />;
|
||||
return <PhoneInput onChange={onChange} value={value} {...rest} />;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -88,6 +91,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||
useEffect(() => {
|
||||
if (selection) {
|
||||
locationFormMethods.setValue("locationType", selection?.value);
|
||||
if (selection?.address) {
|
||||
locationFormMethods.setValue("locationAddress", selection?.address);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selection]);
|
||||
|
@ -149,12 +155,25 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||
name: "locationType",
|
||||
});
|
||||
|
||||
const selectedAddrValue = useWatch({
|
||||
control: locationFormMethods.control,
|
||||
name: "locationAddress",
|
||||
});
|
||||
|
||||
const eventLocationType = getEventLocationType(selectedLocation);
|
||||
|
||||
const defaultLocation = defaultValues?.find(
|
||||
(location: { type: EventLocationType["type"] }) => location.type === eventLocationType?.type
|
||||
(location: { type: EventLocationType["type"]; address?: string }) => {
|
||||
if (location.type === LocationType.InPerson) {
|
||||
return location.type === eventLocationType?.type && location.address === selectedAddrValue;
|
||||
} else {
|
||||
return location.type === eventLocationType?.type;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(defaultLocation);
|
||||
|
||||
const LocationOptions = (() => {
|
||||
if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
|
||||
if (!eventLocationType.variable) {
|
||||
|
|
|
@ -136,9 +136,17 @@ export const EventSetupTab = (
|
|||
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
||||
);
|
||||
|
||||
const openLocationModal = (type: EventLocationType["type"]) => {
|
||||
const openLocationModal = (type: EventLocationType["type"], address = "") => {
|
||||
const option = getLocationFromType(type, locationOptions);
|
||||
setSelectedLocation(option);
|
||||
if (option && option.value === LocationType.InPerson) {
|
||||
const inPersonOption = {
|
||||
...option,
|
||||
address,
|
||||
};
|
||||
setSelectedLocation(inPersonOption);
|
||||
} else {
|
||||
setSelectedLocation(option);
|
||||
}
|
||||
setShowLocationModal(true);
|
||||
};
|
||||
|
||||
|
@ -298,10 +306,14 @@ export const EventSetupTab = (
|
|||
onClick={() => {
|
||||
locationFormMethods.setValue("locationType", location.type);
|
||||
locationFormMethods.unregister("locationLink");
|
||||
locationFormMethods.unregister("locationAddress");
|
||||
if (location.type === LocationType.InPerson) {
|
||||
locationFormMethods.setValue("locationAddress", location.address);
|
||||
} else {
|
||||
locationFormMethods.unregister("locationAddress");
|
||||
}
|
||||
locationFormMethods.unregister("locationPhoneNumber");
|
||||
setEditingLocationType(location.type);
|
||||
openLocationModal(location.type);
|
||||
openLocationModal(location.type, location.address);
|
||||
}}
|
||||
aria-label={t("edit")}
|
||||
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||
|
@ -517,7 +529,18 @@ export const EventSetupTab = (
|
|||
defaultValues={formMethods.getValues("locations")}
|
||||
selection={
|
||||
selectedLocation
|
||||
? { value: selectedLocation.value, label: t(selectedLocation.label), icon: selectedLocation.icon }
|
||||
? selectedLocation.address
|
||||
? {
|
||||
value: selectedLocation.value,
|
||||
label: t(selectedLocation.label),
|
||||
icon: selectedLocation.icon,
|
||||
address: selectedLocation.address,
|
||||
}
|
||||
: {
|
||||
value: selectedLocation.value,
|
||||
label: t(selectedLocation.label),
|
||||
icon: selectedLocation.icon,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
setSelectedLocation={setSelectedLocation}
|
||||
|
|
|
@ -11,6 +11,7 @@ export type LocationOption = {
|
|||
value: EventLocationType["type"];
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
address?: string;
|
||||
};
|
||||
|
||||
export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||
|
|
|
@ -297,6 +297,11 @@ const nextConfig = {
|
|||
destination: "/teams",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/settings/admin",
|
||||
destination: "/settings/admin/flags",
|
||||
permanent: true,
|
||||
},
|
||||
/* V2 testers get redirected to the new settings */
|
||||
{
|
||||
source: "/settings/profile",
|
||||
|
|
|
@ -170,3 +170,30 @@ export const createNewSeatedEventType = async (page: Page, args: { eventTitle: s
|
|||
await page.locator('[data-testid="offer-seats-toggle"]').click();
|
||||
await page.locator('[data-testid="update-eventtype"]').click();
|
||||
};
|
||||
|
||||
export async function gotoRoutingLink({
|
||||
page,
|
||||
formId,
|
||||
queryString = "",
|
||||
}: {
|
||||
page: Page;
|
||||
formId?: string;
|
||||
queryString?: string;
|
||||
}) {
|
||||
let previewLink = null;
|
||||
if (!formId) {
|
||||
// Instead of clicking on the preview link, we are going to the preview link directly because the earlier opens a new tab which is a bit difficult to manage with Playwright
|
||||
const href = await page.locator('[data-testid="form-action-preview"]').getAttribute("href");
|
||||
if (!href) {
|
||||
throw new Error("Preview link not found");
|
||||
}
|
||||
previewLink = href;
|
||||
} else {
|
||||
previewLink = `/forms/${formId}`;
|
||||
}
|
||||
|
||||
await page.goto(`${previewLink}${queryString ? `?${queryString}` : ""}`);
|
||||
|
||||
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
createHttpServer,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
waitFor,
|
||||
gotoRoutingLink,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
@ -387,3 +388,60 @@ test.describe("BOOKING_REQUESTED", async () => {
|
|||
webhookReceiver.close();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("FORM_SUBMITTED", async () => {
|
||||
test("can submit a form and get a submission event", async ({ page, users }) => {
|
||||
const webhookReceiver = createHttpServer();
|
||||
const user = await users.create();
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/settings/teams/new");
|
||||
await page.waitForLoadState("networkidle");
|
||||
const teamName = `${user.username}'s Team`;
|
||||
// Create a new team
|
||||
await page.locator('input[name="name"]').fill(teamName);
|
||||
await page.locator('input[name="slug"]').fill(teamName);
|
||||
await page.locator('button[type="submit"]').click();
|
||||
|
||||
await page.locator("text=Publish team").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Install Routing Forms App
|
||||
await page.goto(`/apps/routing-forms`);
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||
|
||||
await page.click('[data-testid="install-app-button"]');
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto(`/settings/developer/webhooks/new`);
|
||||
|
||||
// Add webhook
|
||||
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
|
||||
await page.fill('[name="secret"]', "secret");
|
||||
await Promise.all([page.click("[type=submit]"), page.goForward()]);
|
||||
|
||||
// Page contains the url
|
||||
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
|
||||
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.goto("/apps/routing-forms/forms");
|
||||
await page.click('[data-testid="new-routing-form"]');
|
||||
await page.fill("input[name]", "TEST FORM");
|
||||
await page.click('[data-testid="add-form"]');
|
||||
await page.waitForSelector('[data-testid="add-field"]');
|
||||
|
||||
const url = page.url();
|
||||
const formId = new URL(url).pathname.split("/").at(-1);
|
||||
|
||||
await gotoRoutingLink({ page, formId: formId });
|
||||
page.click('button[type="submit"]');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(webhookReceiver.requestList.length).toBe(1);
|
||||
});
|
||||
webhookReceiver.close();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -148,6 +148,7 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
|||
|
||||
export type LocationObject = {
|
||||
type: string;
|
||||
address?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
} & Partial<
|
||||
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
|
||||
|
|
|
@ -96,7 +96,7 @@ export default function RoutingForms({
|
|||
return (
|
||||
<ShellMain
|
||||
heading="Routing Forms"
|
||||
CTA={hasPaidPlan && <NewFormButton />}
|
||||
CTA={hasPaidPlan && forms?.length && <NewFormButton />}
|
||||
subtitle={t("routing_forms_description")}>
|
||||
<UpgradeTip
|
||||
title={t("teams_plan_required")}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { expect } from "@playwright/test";
|
|||
|
||||
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
import { gotoRoutingLink } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
function todo(title: string) {
|
||||
// eslint-disable-next-line playwright/no-skipped-test, @typescript-eslint/no-empty-function
|
||||
|
@ -595,33 +596,6 @@ async function selectNewRoute(page: Page, { routeSelectNumber = 1 } = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
async function gotoRoutingLink({
|
||||
page,
|
||||
formId,
|
||||
queryString = "",
|
||||
}: {
|
||||
page: Page;
|
||||
formId?: string;
|
||||
queryString?: string;
|
||||
}) {
|
||||
let previewLink = null;
|
||||
if (!formId) {
|
||||
// Instead of clicking on the preview link, we are going to the preview link directly because the earlier opens a new tab which is a bit difficult to manage with Playwright
|
||||
const href = await page.locator('[data-testid="form-action-preview"]').getAttribute("href");
|
||||
if (!href) {
|
||||
throw new Error("Preview link not found");
|
||||
}
|
||||
previewLink = href;
|
||||
} else {
|
||||
previewLink = `/forms/${formId}`;
|
||||
}
|
||||
|
||||
await page.goto(`${previewLink}${queryString ? `?${queryString}` : ""}`);
|
||||
|
||||
// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
async function saveCurrentForm(page: Page) {
|
||||
await page.click('[data-testid="update-form"]');
|
||||
await page.waitForSelector(".data-testid-toast-success");
|
||||
|
|
|
@ -37,8 +37,8 @@ export const useEvent = () => {
|
|||
export const useScheduleForEvent = ({ prefetchNextMonth }: { prefetchNextMonth?: boolean } = {}) => {
|
||||
const { timezone } = useTimePreferences();
|
||||
const event = useEvent();
|
||||
const [username, eventSlug, month] = useBookerStore(
|
||||
(state) => [state.username, state.eventSlug, state.month],
|
||||
const [username, eventSlug, month, duration] = useBookerStore(
|
||||
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
|
||||
shallow
|
||||
);
|
||||
|
||||
|
@ -49,5 +49,6 @@ export const useScheduleForEvent = ({ prefetchNextMonth }: { prefetchNextMonth?:
|
|||
month,
|
||||
timezone,
|
||||
prefetchNextMonth,
|
||||
duration,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
|
@ -13,6 +14,7 @@ type EmptyCellProps = GridCellToDateProps & {
|
|||
};
|
||||
|
||||
export function EmptyCell(props: EmptyCellProps) {
|
||||
const timeFormat = useTimePreferences((state) => state.timeFormat);
|
||||
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
|
||||
(state) => ({
|
||||
onEmptyCellClick: state.onEmptyCellClick,
|
||||
|
@ -40,7 +42,7 @@ export function EmptyCell(props: EmptyCellProps) {
|
|||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
"group w-full",
|
||||
"group flex w-full items-center justify-center",
|
||||
isDisabled && "pointer-events-none",
|
||||
!isDisabled && "bg-default dark:bg-muted"
|
||||
)}
|
||||
|
@ -54,16 +56,13 @@ export function EmptyCell(props: EmptyCellProps) {
|
|||
rounded-[4px]
|
||||
border-[1px] border-gray-900 py-1 px-[6px] text-xs font-semibold leading-5 group-hover:block group-hover:cursor-pointer"
|
||||
style={{
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height))`,
|
||||
height: `calc(${hoverEventDuration}*var(--one-minute-height) - 2px)`,
|
||||
zIndex: 49,
|
||||
// @TODO: This used to be 90% as per Sean's work. I think this was needed when
|
||||
// multiple events are stacked next to each other. We might need to add this back later.
|
||||
width: "100%",
|
||||
width: "calc(100% - 2px)",
|
||||
}}>
|
||||
<div className=" overflow-ellipsis leading-4">
|
||||
{cellToDate.format("HH:mm")}
|
||||
<span className="ml-2 inline">Click to select</span>
|
||||
</div>
|
||||
<div className=" overflow-ellipsis leading-4">{cellToDate.format(timeFormat)}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ type UseScheduleWithCacheArgs = {
|
|||
month?: string | null;
|
||||
timezone?: string | null;
|
||||
prefetchNextMonth?: boolean;
|
||||
duration?: number | null;
|
||||
};
|
||||
|
||||
export const useSchedule = ({
|
||||
|
@ -17,6 +18,7 @@ export const useSchedule = ({
|
|||
eventSlug,
|
||||
eventId,
|
||||
prefetchNextMonth,
|
||||
duration,
|
||||
}: UseScheduleWithCacheArgs) => {
|
||||
const monthDayjs = month ? dayjs(month) : dayjs();
|
||||
const nextMonthDayjs = monthDayjs.add(1, "month");
|
||||
|
@ -35,6 +37,7 @@ export const useSchedule = ({
|
|||
endTime: (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString(),
|
||||
timeZone: timezone!,
|
||||
eventTypeId: eventId!,
|
||||
duration: duration ? `${duration}` : undefined,
|
||||
},
|
||||
{
|
||||
refetchOnWindowFocus: false,
|
||||
|
|
|
@ -7,6 +7,7 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
|
|||
WebhookTriggerEvents.BOOKING_CANCELLED,
|
||||
WebhookTriggerEvents.BOOKING_CREATED,
|
||||
WebhookTriggerEvents.BOOKING_RESCHEDULED,
|
||||
WebhookTriggerEvents.BOOKING_PAID,
|
||||
WebhookTriggerEvents.MEETING_ENDED,
|
||||
WebhookTriggerEvents.BOOKING_REQUESTED,
|
||||
WebhookTriggerEvents.BOOKING_REJECTED,
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
-- AlterEnum
|
||||
-- This migration adds more than one value to an enum.
|
||||
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||
-- in a single migration. This can be worked around by creating
|
||||
-- multiple migrations, each migration adding only one value to
|
||||
-- the enum.
|
||||
|
||||
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_PAID';
|
|
@ -512,6 +512,7 @@ enum PaymentOption {
|
|||
|
||||
enum WebhookTriggerEvents {
|
||||
BOOKING_CREATED
|
||||
BOOKING_PAID
|
||||
BOOKING_RESCHEDULED
|
||||
BOOKING_REQUESTED
|
||||
BOOKING_CANCELLED
|
||||
|
|
|
@ -3,7 +3,9 @@ import { z } from "zod";
|
|||
import appStore from "@calcom/app-store";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendNoShowFeeChargedEmail } from "@calcom/emails";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import sendPayload from "@calcom/lib/server/webhooks/sendPayload";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
@ -33,11 +35,13 @@ export const paymentsRouter = router({
|
|||
},
|
||||
});
|
||||
|
||||
const payment = booking.payment[0];
|
||||
|
||||
if (!booking) {
|
||||
throw new Error("Booking not found");
|
||||
}
|
||||
|
||||
if (booking.payment[0].success) {
|
||||
if (payment.success) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: `The no show fee for ${booking.id} has already been charged.`,
|
||||
|
@ -77,16 +81,16 @@ export const paymentsRouter = router({
|
|||
},
|
||||
attendees: attendeesList,
|
||||
paymentInfo: {
|
||||
amount: booking.payment[0].amount,
|
||||
currency: booking.payment[0].currency,
|
||||
paymentOption: booking.payment[0].paymentOption,
|
||||
amount: payment.amount,
|
||||
currency: payment.currency,
|
||||
paymentOption: payment.paymentOption,
|
||||
},
|
||||
};
|
||||
|
||||
const paymentCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
appId: booking.payment[0].appId,
|
||||
appId: payment.appId,
|
||||
},
|
||||
include: {
|
||||
app: true,
|
||||
|
@ -107,12 +111,32 @@ export const paymentsRouter = router({
|
|||
const paymentInstance = new PaymentService(paymentCredential);
|
||||
|
||||
try {
|
||||
const paymentData = await paymentInstance.chargeCard(booking.payment[0]);
|
||||
const paymentData = await paymentInstance.chargeCard(payment);
|
||||
|
||||
if (!paymentData) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` });
|
||||
}
|
||||
|
||||
const subscriberOptions = {
|
||||
userId: ctx.user.id || 0,
|
||||
eventTypeId: booking.eventTypeId || 0,
|
||||
triggerEvent: WebhookTriggerEvents.BOOKING_PAID,
|
||||
};
|
||||
|
||||
const subscribers = await getWebhooks(subscriberOptions);
|
||||
|
||||
await Promise.all(
|
||||
subscribers.map(async (subscriber) => {
|
||||
sendPayload(subscriber.secret, WebhookTriggerEvents.BOOKING_PAID, {
|
||||
...evt,
|
||||
bookingId: booking.id,
|
||||
paymentId: payment.id,
|
||||
paymentData,
|
||||
eventTypeId: subscriberOptions.eventTypeId,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
await sendNoShowFeeChargedEmail(attendeesListPromises[0], evt);
|
||||
|
||||
return paymentData;
|
||||
|
|
Loading…
Reference in New Issue
Block a user