Compare commits
11 Commits
feat/add-s
...
main
Author | SHA1 | Date |
---|---|---|
Keith Williams | 51fd4102ae | |
Ritesh Kumar | 9d1ef0a649 | |
gitstart-app[bot] | f80dc0738a | |
Peer Richelsen | 678ab3f453 | |
Syed Ali Shahbaz | 199d3e4c3f | |
Siddharth Movaliya | 79a6aef0e7 | |
Udit Takkar | 4d49fb0636 | |
Udit Takkar | 0be1387d0f | |
Morgan | 58ab278813 | |
Alex van Andel | 4de142cb7c | |
Hariom Balhara | b4d27a9326 |
|
@ -1,18 +0,0 @@
|
||||||
name: Auto Comment Merge Conflicts
|
|
||||||
on: push
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
auto-comment-merge-conflicts:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: codytseng/auto-comment-merge-conflicts@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
comment-body: "Hey there, there is a merge conflict, can you take a look?"
|
|
||||||
wait-ms: 3000
|
|
||||||
max-retries: 5
|
|
||||||
label-name: "🚨 merge conflict"
|
|
||||||
ignore-authors: dependabot,otherAuthor
|
|
|
@ -3,7 +3,6 @@ import { z } from "zod";
|
||||||
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
|
import { _DestinationCalendarModel as DestinationCalendar } from "@calcom/prisma/zod";
|
||||||
|
|
||||||
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
|
export const schemaDestinationCalendarBaseBodyParams = DestinationCalendar.pick({
|
||||||
credentialId: true,
|
|
||||||
integration: true,
|
integration: true,
|
||||||
externalId: true,
|
externalId: true,
|
||||||
eventTypeId: true,
|
eventTypeId: true,
|
||||||
|
@ -15,7 +14,6 @@ const schemaDestinationCalendarCreateParams = z
|
||||||
.object({
|
.object({
|
||||||
integration: z.string(),
|
integration: z.string(),
|
||||||
externalId: z.string(),
|
externalId: z.string(),
|
||||||
credentialId: z.number(),
|
|
||||||
eventTypeId: z.number().optional(),
|
eventTypeId: z.number().optional(),
|
||||||
bookingId: z.number().optional(),
|
bookingId: z.number().optional(),
|
||||||
userId: z.number().optional(),
|
userId: z.number().optional(),
|
||||||
|
@ -47,5 +45,4 @@ export const schemaDestinationCalendarReadPublic = DestinationCalendar.pick({
|
||||||
eventTypeId: true,
|
eventTypeId: true,
|
||||||
bookingId: true,
|
bookingId: true,
|
||||||
userId: true,
|
userId: true,
|
||||||
credentialId: true,
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
|
import type { Prisma } from "@prisma/client";
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import type { PrismaClient } from "@calcom/prisma";
|
||||||
|
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
schemaDestinationCalendarEditBodyParams,
|
schemaDestinationCalendarEditBodyParams,
|
||||||
|
@ -56,16 +62,251 @@ import { schemaQueryIdParseInt } from "~/lib/validations/shared/queryIdTransform
|
||||||
* 404:
|
* 404:
|
||||||
* description: Destination calendar not found
|
* description: Destination calendar not found
|
||||||
*/
|
*/
|
||||||
|
type DestinationCalendarType = {
|
||||||
|
userId?: number | null;
|
||||||
|
eventTypeId?: number | null;
|
||||||
|
credentialId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UserCredentialType = {
|
||||||
|
id: number;
|
||||||
|
appId: string | null;
|
||||||
|
type: string;
|
||||||
|
userId: number | null;
|
||||||
|
user: {
|
||||||
|
email: string;
|
||||||
|
} | null;
|
||||||
|
teamId: number | null;
|
||||||
|
key: Prisma.JsonValue;
|
||||||
|
invalid: boolean | null;
|
||||||
|
};
|
||||||
|
|
||||||
export async function patchHandler(req: NextApiRequest) {
|
export async function patchHandler(req: NextApiRequest) {
|
||||||
const { prisma, query, body } = req;
|
const { userId, isAdmin, prisma, query, body } = req;
|
||||||
const { id } = schemaQueryIdParseInt.parse(query);
|
const { id } = schemaQueryIdParseInt.parse(query);
|
||||||
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
const parsedBody = schemaDestinationCalendarEditBodyParams.parse(body);
|
||||||
|
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
||||||
|
|
||||||
|
validateIntegrationInput(parsedBody);
|
||||||
|
const destinationCalendarObject: DestinationCalendarType = await getDestinationCalendar(id, prisma);
|
||||||
|
await validateRequestAndOwnership({ destinationCalendarObject, parsedBody, assignedUserId, prisma });
|
||||||
|
|
||||||
|
const userCredentials = await getUserCredentials({
|
||||||
|
credentialId: destinationCalendarObject.credentialId,
|
||||||
|
userId: assignedUserId,
|
||||||
|
prisma,
|
||||||
|
});
|
||||||
|
const credentialId = await verifyCredentialsAndGetId({
|
||||||
|
parsedBody,
|
||||||
|
userCredentials,
|
||||||
|
currentCredentialId: destinationCalendarObject.credentialId,
|
||||||
|
});
|
||||||
|
// If the user has passed eventTypeId, we need to remove userId from the update data to make sure we don't link it to user as well
|
||||||
|
if (parsedBody.eventTypeId) parsedBody.userId = undefined;
|
||||||
const destinationCalendar = await prisma.destinationCalendar.update({
|
const destinationCalendar = await prisma.destinationCalendar.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: parsedBody,
|
data: { ...parsedBody, credentialId },
|
||||||
});
|
});
|
||||||
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
|
return { destinationCalendar: schemaDestinationCalendarReadPublic.parse(destinationCalendar) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves user credentials associated with a given credential ID and user ID and validates if the credentials belong to this user
|
||||||
|
*
|
||||||
|
* @param credentialId - The ID of the credential to fetch. If not provided, an error is thrown.
|
||||||
|
* @param userId - The user ID against which the credentials need to be verified.
|
||||||
|
* @param prisma - An instance of PrismaClient for database operations.
|
||||||
|
*
|
||||||
|
* @returns - An array containing the matching user credentials.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If `credentialId` is not provided or no associated credentials are found in the database.
|
||||||
|
*/
|
||||||
|
async function getUserCredentials({
|
||||||
|
credentialId,
|
||||||
|
userId,
|
||||||
|
prisma,
|
||||||
|
}: {
|
||||||
|
credentialId: number | null;
|
||||||
|
userId: number;
|
||||||
|
prisma: PrismaClient;
|
||||||
|
}) {
|
||||||
|
if (!credentialId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `Destination calendar missing credential id`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const userCredentials = await prisma.credential.findMany({
|
||||||
|
where: { id: credentialId, userId },
|
||||||
|
select: credentialForCalendarServiceSelect,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userCredentials || userCredentials.length === 0) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: `Bad request, no associated credentials found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return userCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies the provided credentials and retrieves the associated credential ID.
|
||||||
|
*
|
||||||
|
* This function checks if the `integration` and `externalId` properties from the parsed body are present.
|
||||||
|
* If both properties exist, it fetches the connected calendar credentials using the provided user credentials
|
||||||
|
* and checks for a matching external ID and integration from the list of connected calendars.
|
||||||
|
*
|
||||||
|
* If a match is found, it updates the `credentialId` with the one from the connected calendar.
|
||||||
|
* Otherwise, it throws an HTTP error with a 400 status indicating an invalid credential ID.
|
||||||
|
*
|
||||||
|
* If the parsed body does not contain the necessary properties, the function
|
||||||
|
* returns the `credentialId` from the destination calendar object.
|
||||||
|
*
|
||||||
|
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||||
|
* Checked if it contain properties like `integration` and `externalId`.
|
||||||
|
* @param userCredentials - An array of user credentials used to fetch the connected calendar credentials.
|
||||||
|
* @param destinationCalendarObject - An object representing the destination calendar. Primarily used
|
||||||
|
* to fetch the default `credentialId`.
|
||||||
|
*
|
||||||
|
* @returns - The verified `credentialId` either from the matched connected calendar in case of updating the destination calendar,
|
||||||
|
* or the provided destination calendar object in other cases.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If no matching connected calendar is found for the given `integration` and `externalId`.
|
||||||
|
*/
|
||||||
|
async function verifyCredentialsAndGetId({
|
||||||
|
parsedBody,
|
||||||
|
userCredentials,
|
||||||
|
currentCredentialId,
|
||||||
|
}: {
|
||||||
|
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||||
|
userCredentials: UserCredentialType[];
|
||||||
|
currentCredentialId: number | null;
|
||||||
|
}) {
|
||||||
|
if (parsedBody.integration && parsedBody.externalId) {
|
||||||
|
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||||
|
|
||||||
|
const { connectedCalendars } = await getConnectedCalendars(
|
||||||
|
calendarCredentials,
|
||||||
|
[],
|
||||||
|
parsedBody.externalId
|
||||||
|
);
|
||||||
|
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||||
|
const calendar = eligibleCalendars?.find(
|
||||||
|
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!calendar?.credentialId)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, credential id invalid",
|
||||||
|
});
|
||||||
|
return calendar?.credentialId;
|
||||||
|
}
|
||||||
|
return currentCredentialId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the request for updating a destination calendar.
|
||||||
|
*
|
||||||
|
* This function checks the validity of the provided eventTypeId against the existing destination calendar object
|
||||||
|
* in the sense that if the destination calendar is not linked to an event type, the eventTypeId can not be provided.
|
||||||
|
*
|
||||||
|
* It also ensures that the eventTypeId, if provided, belongs to the assigned user.
|
||||||
|
*
|
||||||
|
* @param destinationCalendarObject - An object representing the destination calendar.
|
||||||
|
* @param parsedBody - The parsed body from the incoming request, validated against a predefined schema.
|
||||||
|
* @param assignedUserId - The user ID assigned for the operation, which might be an admin or a regular user.
|
||||||
|
* @param prisma - An instance of PrismaClient for database operations.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If the validation fails or inconsistencies are detected in the request data.
|
||||||
|
*/
|
||||||
|
async function validateRequestAndOwnership({
|
||||||
|
destinationCalendarObject,
|
||||||
|
parsedBody,
|
||||||
|
assignedUserId,
|
||||||
|
prisma,
|
||||||
|
}: {
|
||||||
|
destinationCalendarObject: DestinationCalendarType;
|
||||||
|
parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>;
|
||||||
|
assignedUserId: number;
|
||||||
|
prisma: PrismaClient;
|
||||||
|
}) {
|
||||||
|
if (parsedBody.eventTypeId) {
|
||||||
|
if (!destinationCalendarObject.eventTypeId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: `The provided destination calendar can not be linked to an event type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userEventType = await prisma.eventType.findFirst({
|
||||||
|
where: { id: parsedBody.eventTypeId },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userEventType || userEventType.userId !== assignedUserId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `Event type with ID ${parsedBody.eventTypeId} not found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsedBody.eventTypeId) {
|
||||||
|
if (destinationCalendarObject.eventTypeId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: `The provided destination calendar can only be linked to an event type`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (destinationCalendarObject.userId !== assignedUserId) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: `Forbidden`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the destination calendar based on the provided ID as the path parameter, specifically `credentialId` and `eventTypeId`.
|
||||||
|
*
|
||||||
|
* If no matching destination calendar is found for the provided ID, an HTTP error with a 404 status
|
||||||
|
* indicating that the desired destination calendar was not found is thrown.
|
||||||
|
*
|
||||||
|
* @param id - The ID of the destination calendar to be retrieved.
|
||||||
|
* @param prisma - An instance of PrismaClient for database operations.
|
||||||
|
*
|
||||||
|
* @returns - An object containing details of the matching destination calendar, specifically `credentialId` and `eventTypeId`.
|
||||||
|
*
|
||||||
|
* @throws HttpError - If no destination calendar matches the provided ID.
|
||||||
|
*/
|
||||||
|
async function getDestinationCalendar(id: number, prisma: PrismaClient) {
|
||||||
|
const destinationCalendarObject = await prisma.destinationCalendar.findFirst({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
select: { userId: true, eventTypeId: true, credentialId: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!destinationCalendarObject) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: `Destination calendar with ID ${id} not found`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return destinationCalendarObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateIntegrationInput(parsedBody: z.infer<typeof schemaDestinationCalendarEditBodyParams>) {
|
||||||
|
if (parsedBody.integration && !parsedBody.externalId) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: "External Id is required with integration value" });
|
||||||
|
}
|
||||||
|
if (!parsedBody.integration && parsedBody.externalId) {
|
||||||
|
throw new HttpError({ statusCode: 400, message: "Integration value is required with external ID" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defaultResponder(patchHandler);
|
export default defaultResponder(patchHandler);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
|
import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
import { defaultResponder } from "@calcom/lib/server";
|
||||||
|
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
schemaDestinationCalendarReadPublic,
|
schemaDestinationCalendarReadPublic,
|
||||||
|
@ -38,9 +40,6 @@ import {
|
||||||
* externalId:
|
* externalId:
|
||||||
* type: string
|
* type: string
|
||||||
* description: 'The external ID of the integration'
|
* description: 'The external ID of the integration'
|
||||||
* credentialId:
|
|
||||||
* type: integer
|
|
||||||
* description: 'The credential ID it is associated with'
|
|
||||||
* eventTypeId:
|
* eventTypeId:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: 'The ID of the eventType it is associated with'
|
* description: 'The ID of the eventType it is associated with'
|
||||||
|
@ -65,20 +64,38 @@ async function postHandler(req: NextApiRequest) {
|
||||||
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
const parsedBody = schemaDestinationCalendarCreateBodyParams.parse(body);
|
||||||
await checkPermissions(req, userId);
|
await checkPermissions(req, userId);
|
||||||
|
|
||||||
const assignedUserId = isAdmin ? parsedBody.userId || userId : userId;
|
const assignedUserId = isAdmin && parsedBody.userId ? parsedBody.userId : userId;
|
||||||
|
|
||||||
/* Check if credentialId data matches the ownership and integration passed in */
|
/* Check if credentialId data matches the ownership and integration passed in */
|
||||||
const credential = await prisma.credential.findFirst({
|
const userCredentials = await prisma.credential.findMany({
|
||||||
where: { type: parsedBody.integration, userId: assignedUserId },
|
where: {
|
||||||
select: { id: true, type: true, userId: true },
|
type: parsedBody.integration,
|
||||||
|
userId: assignedUserId,
|
||||||
|
},
|
||||||
|
select: credentialForCalendarServiceSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!credential)
|
if (userCredentials.length === 0)
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: "Bad request, credential id invalid",
|
message: "Bad request, credential id invalid",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const calendarCredentials = getCalendarCredentials(userCredentials);
|
||||||
|
|
||||||
|
const { connectedCalendars } = await getConnectedCalendars(calendarCredentials, [], parsedBody.externalId);
|
||||||
|
|
||||||
|
const eligibleCalendars = connectedCalendars[0]?.calendars?.filter((calendar) => !calendar.readOnly);
|
||||||
|
const calendar = eligibleCalendars?.find(
|
||||||
|
(c) => c.externalId === parsedBody.externalId && c.integration === parsedBody.integration
|
||||||
|
);
|
||||||
|
if (!calendar?.credentialId)
|
||||||
|
throw new HttpError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Bad request, credential id invalid",
|
||||||
|
});
|
||||||
|
const credentialId = calendar.credentialId;
|
||||||
|
|
||||||
if (parsedBody.eventTypeId) {
|
if (parsedBody.eventTypeId) {
|
||||||
const eventType = await prisma.eventType.findFirst({
|
const eventType = await prisma.eventType.findFirst({
|
||||||
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
|
where: { id: parsedBody.eventTypeId, userId: parsedBody.userId },
|
||||||
|
@ -91,7 +108,9 @@ async function postHandler(req: NextApiRequest) {
|
||||||
parsedBody.userId = undefined;
|
parsedBody.userId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const destination_calendar = await prisma.destinationCalendar.create({ data: { ...parsedBody } });
|
const destination_calendar = await prisma.destinationCalendar.create({
|
||||||
|
data: { ...parsedBody, credentialId },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
destinationCalendar: schemaDestinationCalendarReadPublic.parse(destination_calendar),
|
||||||
|
|
|
@ -141,17 +141,6 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
: []),
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const showRecordingActions: ActionType[] = [
|
|
||||||
{
|
|
||||||
id: "view_recordings",
|
|
||||||
label: t("view_recordings"),
|
|
||||||
onClick: () => {
|
|
||||||
setViewRecordingsDialogIsOpen(true);
|
|
||||||
},
|
|
||||||
disabled: mutation.isLoading,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let bookedActions: ActionType[] = [
|
let bookedActions: ActionType[] = [
|
||||||
{
|
{
|
||||||
id: "cancel",
|
id: "cancel",
|
||||||
|
@ -270,11 +259,21 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
const bookingLink = buildBookingLink();
|
const bookingLink = buildBookingLink();
|
||||||
|
|
||||||
const title = booking.title;
|
const title = booking.title;
|
||||||
// To be used after we run query on legacy bookings
|
|
||||||
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
|
||||||
|
|
||||||
const showRecordingsButtons =
|
const showRecordingsButtons = !!(booking.isRecorded && isPast && isConfirmed);
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
const checkForRecordingsButton =
|
||||||
|
!showRecordingsButtons && (booking.location === "integrations:daily" || booking?.location?.trim() === "");
|
||||||
|
|
||||||
|
const showRecordingActions: ActionType[] = [
|
||||||
|
{
|
||||||
|
id: checkForRecordingsButton ? "check_for_recordings" : "view_recordings",
|
||||||
|
label: checkForRecordingsButton ? t("check_for_recordings") : t("view_recordings"),
|
||||||
|
onClick: () => {
|
||||||
|
setViewRecordingsDialogIsOpen(true);
|
||||||
|
},
|
||||||
|
disabled: mutation.isLoading,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -299,7 +298,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
paymentCurrency={booking.payment[0].currency}
|
paymentCurrency={booking.payment[0].currency}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showRecordingsButtons && (
|
{(showRecordingsButtons || checkForRecordingsButton) && (
|
||||||
<ViewRecordingsDialog
|
<ViewRecordingsDialog
|
||||||
booking={booking}
|
booking={booking}
|
||||||
isOpenDialog={viewRecordingsDialogIsOpen}
|
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||||
|
@ -469,7 +468,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||||
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
{(showRecordingsButtons || checkForRecordingsButton) && (
|
||||||
|
<TableActions actions={showRecordingActions} />
|
||||||
|
)}
|
||||||
{isCancelled && booking.rescheduled && (
|
{isCancelled && booking.rescheduled && (
|
||||||
<div className="hidden h-full items-center md:flex">
|
<div className="hidden h-full items-center md:flex">
|
||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.4.5",
|
"version": "3.4.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|
|
@ -62,6 +62,46 @@ const triggerWebhook = async ({
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkIfUserIsPartOfTheSameTeam = async (
|
||||||
|
teamId: number | undefined | null,
|
||||||
|
userId: number,
|
||||||
|
userEmail: string | undefined | null
|
||||||
|
) => {
|
||||||
|
if (!teamId) return false;
|
||||||
|
|
||||||
|
const getUserQuery = () => {
|
||||||
|
if (!!userEmail) {
|
||||||
|
return {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
id: userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: userEmail,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const team = await prisma.team.findFirst({
|
||||||
|
where: {
|
||||||
|
id: teamId,
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
user: getUserQuery(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!team;
|
||||||
|
};
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||||
|
@ -137,13 +177,23 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|
||||||
const isUserAttendeeOrOrganiser =
|
const isUserAttendeeOrOrganiser =
|
||||||
booking?.user?.id === session.user.id ||
|
booking?.user?.id === session.user.id ||
|
||||||
attendeesList.find((attendee) => attendee.id === session.user.id);
|
attendeesList.find(
|
||||||
|
(attendee) => attendee.id === session.user.id || attendee.email === session.user.email
|
||||||
|
);
|
||||||
|
|
||||||
if (!isUserAttendeeOrOrganiser) {
|
if (!isUserAttendeeOrOrganiser) {
|
||||||
|
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam(
|
||||||
|
booking?.eventType?.teamId,
|
||||||
|
session.user.id,
|
||||||
|
session.user.email
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isUserMemberOfTheTeam) {
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
message: "Unauthorised",
|
message: "Unauthorised",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await prisma.booking.update({
|
await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
|
@ -202,7 +252,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|
||||||
return res.status(403).json({ message: "User does not have team plan to send out emails" });
|
return res.status(403).json({ message: "User does not have team plan to send out emails" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("something_went_wrong", err);
|
console.warn("Error in /recorded-daily-video", err);
|
||||||
return res.status(500).json({ message: "something went wrong" });
|
return res.status(500).json({ message: "something went wrong" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -342,14 +342,17 @@ export default function Success(props: SuccessProps) {
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "text-center" : "",
|
shouldAlignCentrally ? "text-center" : "",
|
||||||
"flex items-end justify-center px-4 pb-20 pt-4 sm:block sm:p-0"
|
"flex items-end justify-center px-4 pb-20 pt-4 sm:flex sm:p-0"
|
||||||
)}>
|
)}>
|
||||||
<div
|
<div
|
||||||
className={classNames("my-4 transition-opacity sm:my-0", isEmbed ? "" : " inset-0")}
|
className={classNames(
|
||||||
|
"main my-4 flex flex-col transition-opacity sm:my-0 ",
|
||||||
|
isEmbed ? "" : " inset-0"
|
||||||
|
)}
|
||||||
aria-hidden="true">
|
aria-hidden="true">
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
"inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
||||||
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
|
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
|
||||||
"px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
"px-8 pb-4 pt-5 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -164,14 +164,13 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
||||||
<>
|
<>
|
||||||
<Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} />
|
<Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} />
|
||||||
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
||||||
<div>
|
<div className="border-subtle rounded-b-xl border border-t-0 px-4 py-6 sm:px-6">
|
||||||
<div className="mt-6">
|
|
||||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||||
{t("account_managed_by_identity_provider", {
|
{t("account_managed_by_identity_provider", {
|
||||||
provider: identityProviderNameMap[user.identityProvider],
|
provider: identityProviderNameMap[user.identityProvider],
|
||||||
})}
|
})}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
|
||||||
<p className="text-subtle mt-1 text-sm">
|
<p className="text-subtle mt-1 text-sm">
|
||||||
{t("account_managed_by_identity_provider_description", {
|
{t("account_managed_by_identity_provider_description", {
|
||||||
provider: identityProviderNameMap[user.identityProvider],
|
provider: identityProviderNameMap[user.identityProvider],
|
||||||
|
@ -180,7 +179,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Form form={formMethods} handleSubmit={handleSubmit}>
|
<Form form={formMethods} handleSubmit={handleSubmit}>
|
||||||
<div className="border-x px-4 py-6 sm:px-6">
|
<div className="border-subtle border-x px-4 py-6 sm:px-6">
|
||||||
{formMethods.formState.errors.apiError && (
|
{formMethods.formState.errors.apiError && (
|
||||||
<div className="pb-6">
|
<div className="pb-6">
|
||||||
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
|
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
|
||||||
|
|
|
@ -0,0 +1,441 @@
|
||||||
|
import { loginUser } from "../fixtures/regularBookings";
|
||||||
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
test.describe("Booking With Phone Question and Each Other Question", () => {
|
||||||
|
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||||
|
await loginUser(users);
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await bookingPage.goToEventType("30 min");
|
||||||
|
await bookingPage.goToTab("event_advanced_tab_title");
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Address Question", () => {
|
||||||
|
test("Select required and Address required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and Address question (both required)",
|
||||||
|
secondQuestion: "address",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Address not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and Address question (only select required)",
|
||||||
|
secondQuestion: "address",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and checkbox group Question", () => {
|
||||||
|
test("Select required and checkbox group required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and Checkbox question (both required)",
|
||||||
|
secondQuestion: "checkbox",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and checkbox group not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and Checkbox question (only Select required)",
|
||||||
|
secondQuestion: "checkbox",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and checkbox Question", () => {
|
||||||
|
test("Select required and checkbox required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and boolean question (both required)",
|
||||||
|
secondQuestion: "boolean",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and checkbox not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and boolean question (only select required)",
|
||||||
|
secondQuestion: "boolean",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Long text Question", () => {
|
||||||
|
test("Select required and Long text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and textarea question (both required)",
|
||||||
|
secondQuestion: "textarea",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Long text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and textarea question (only select required)",
|
||||||
|
secondQuestion: "textarea",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Multi email Question", () => {
|
||||||
|
test("Select required and Multi email required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
true,
|
||||||
|
"multiemail test"
|
||||||
|
);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and multiemail question (both required)",
|
||||||
|
secondQuestion: "multiemail",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Multi email not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
"multiemail",
|
||||||
|
"multiemail-test",
|
||||||
|
"multiemail test",
|
||||||
|
false,
|
||||||
|
"multiemail test"
|
||||||
|
);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and multiemail question (only select required)",
|
||||||
|
secondQuestion: "multiemail",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and multiselect Question", () => {
|
||||||
|
test("Select required and multiselect text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and multiselect question (both required)",
|
||||||
|
secondQuestion: "multiselect",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and multiselect text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and multiselect question (only select required)",
|
||||||
|
secondQuestion: "multiselect",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Number Question", () => {
|
||||||
|
test("Select required and Number required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and number question (both required)",
|
||||||
|
secondQuestion: "number",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Number not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and number question (only select required)",
|
||||||
|
secondQuestion: "number",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Phone Question", () => {
|
||||||
|
test("Select required and select required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and phone question (both required)",
|
||||||
|
secondQuestion: "phone",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Phone not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and phone question (only select required)",
|
||||||
|
secondQuestion: "phone",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Radio group Question", () => {
|
||||||
|
test("Select required and Radio group required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and radio question (both required)",
|
||||||
|
secondQuestion: "radio",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Radio group not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and radio question (only select required)",
|
||||||
|
secondQuestion: "radio",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With Select Question and Short text question", () => {
|
||||||
|
test("Select required and Short text required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and text question (both required)",
|
||||||
|
secondQuestion: "text",
|
||||||
|
options: bookingOptions,
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Select and Short text not required", async ({ bookingPage }) => {
|
||||||
|
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||||
|
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.fillAndConfirmBooking({
|
||||||
|
eventTypePage,
|
||||||
|
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||||
|
question: "select",
|
||||||
|
fillText: "Test Select question and text question (only select required)",
|
||||||
|
secondQuestion: "text",
|
||||||
|
options: { ...bookingOptions, isRequired: false },
|
||||||
|
});
|
||||||
|
await bookingPage.rescheduleBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||||
|
await bookingPage.cancelBooking(eventTypePage);
|
||||||
|
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,7 @@
|
||||||
import { expect, type Page } from "@playwright/test";
|
import { expect, type Page } from "@playwright/test";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
|
||||||
import type { createUsersFixture } from "./users";
|
import type { createUsersFixture } from "./users";
|
||||||
|
|
||||||
const reschedulePlaceholderText = "Let others know why you need to reschedule";
|
const reschedulePlaceholderText = "Let others know why you need to reschedule";
|
||||||
|
@ -38,6 +40,12 @@ type fillAndConfirmBookingParams = {
|
||||||
|
|
||||||
type UserFixture = ReturnType<typeof createUsersFixture>;
|
type UserFixture = ReturnType<typeof createUsersFixture>;
|
||||||
|
|
||||||
|
function isLastDayOfMonth(): boolean {
|
||||||
|
const today = dayjs();
|
||||||
|
const endOfMonth = today.endOf("month");
|
||||||
|
return today.isSame(endOfMonth, "day");
|
||||||
|
}
|
||||||
|
|
||||||
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
|
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
|
||||||
const questionActions: QuestionActions = {
|
const questionActions: QuestionActions = {
|
||||||
phone: async () => {
|
phone: async () => {
|
||||||
|
@ -103,7 +111,6 @@ const fillQuestion = async (eventTypePage: Page, questionType: string, customLoc
|
||||||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
|
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (questionActions[questionType]) {
|
if (questionActions[questionType]) {
|
||||||
await questionActions[questionType]();
|
await questionActions[questionType]();
|
||||||
}
|
}
|
||||||
|
@ -114,6 +121,17 @@ export async function loginUser(users: UserFixture) {
|
||||||
await pro.apiLogin();
|
await pro.apiLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => {
|
||||||
|
try {
|
||||||
|
if (isLastDayOfMonth()) {
|
||||||
|
await eventTypePage.getByTestId("view_next_month").waitFor({ timeout: 6000 });
|
||||||
|
await eventTypePage.getByTestId("view_next_month").click();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.info("No need to click on view next month button");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export function createBookingPageFixture(page: Page) {
|
export function createBookingPageFixture(page: Page) {
|
||||||
return {
|
return {
|
||||||
goToEventType: async (eventType: string) => {
|
goToEventType: async (eventType: string) => {
|
||||||
|
@ -154,19 +172,13 @@ export function createBookingPageFixture(page: Page) {
|
||||||
return eventtypePromise;
|
return eventtypePromise;
|
||||||
},
|
},
|
||||||
selectTimeSlot: async (eventTypePage: Page) => {
|
selectTimeSlot: async (eventTypePage: Page) => {
|
||||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
|
||||||
}
|
|
||||||
await eventTypePage.getByTestId("time").first().click();
|
await eventTypePage.getByTestId("time").first().click();
|
||||||
},
|
},
|
||||||
clickReschedule: async () => {
|
clickReschedule: async () => {
|
||||||
await page.getByText("Reschedule").click();
|
await page.getByText("Reschedule").click();
|
||||||
},
|
},
|
||||||
navigateToAvailableTimeSlot: async () => {
|
|
||||||
while (await page.getByRole("button", { name: "View next" }).isVisible()) {
|
|
||||||
await page.getByRole("button", { name: "View next" }).click();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectFirstAvailableTime: async () => {
|
selectFirstAvailableTime: async () => {
|
||||||
await page.getByTestId("time").first().click();
|
await page.getByTestId("time").first().click();
|
||||||
},
|
},
|
||||||
|
@ -186,6 +198,7 @@ export function createBookingPageFixture(page: Page) {
|
||||||
},
|
},
|
||||||
|
|
||||||
rescheduleBooking: async (eventTypePage: Page) => {
|
rescheduleBooking: async (eventTypePage: Page) => {
|
||||||
|
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||||
await eventTypePage.getByText("Reschedule").click();
|
await eventTypePage.getByText("Reschedule").click();
|
||||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
||||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
||||||
|
@ -222,7 +235,7 @@ export function createBookingPageFixture(page: Page) {
|
||||||
|
|
||||||
// Change the selector for specifics cases related to select question
|
// Change the selector for specifics cases related to select question
|
||||||
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
|
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
|
||||||
question === "select" && ["multiemail", "multiselect"].includes(secondQuestion);
|
question === "select" && ["multiemail", "multiselect", "address"].includes(secondQuestion);
|
||||||
|
|
||||||
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
||||||
question === "radio" && secondQuestion === "checkbox";
|
question === "radio" && secondQuestion === "checkbox";
|
||||||
|
|
|
@ -1296,6 +1296,7 @@
|
||||||
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
|
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
|
||||||
"check_for_conflicts": "Check for conflicts",
|
"check_for_conflicts": "Check for conflicts",
|
||||||
"view_recordings": "View recordings",
|
"view_recordings": "View recordings",
|
||||||
|
"check_for_recordings":"Check for recordings",
|
||||||
"adding_events_to": "Adding events to",
|
"adding_events_to": "Adding events to",
|
||||||
"follow_system_preferences": "Follow system preferences",
|
"follow_system_preferences": "Follow system preferences",
|
||||||
"custom_brand_colors": "Custom brand colors",
|
"custom_brand_colors": "Custom brand colors",
|
||||||
|
|
|
@ -161,8 +161,8 @@ const EmailStep = (props: { translationString: string; iconsrc: string }) => {
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#E5E7EB",
|
backgroundColor: "#E5E7EB",
|
||||||
borderRadius: "48px",
|
borderRadius: "48px",
|
||||||
height: "48px",
|
minHeight: "48px",
|
||||||
width: "48px",
|
minWidth: "48px",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
|
@ -50,8 +50,13 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
||||||
return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format });
|
return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected getLocale(): string {
|
||||||
|
return this.calEvent.organizer.language.locale;
|
||||||
|
}
|
||||||
|
|
||||||
protected getFormattedDate() {
|
protected getFormattedDate() {
|
||||||
const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR;
|
const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR;
|
||||||
|
|
||||||
return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd(
|
return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd(
|
||||||
organizerTimeFormat
|
organizerTimeFormat
|
||||||
)}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t(
|
)}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t(
|
||||||
|
|
|
@ -95,7 +95,7 @@ const NoAvailabilityOverlay = ({
|
||||||
return (
|
return (
|
||||||
<div className="bg-muted border-subtle absolute left-1/2 top-40 -mt-10 w-max -translate-x-1/2 -translate-y-1/2 transform rounded-md border p-8 shadow-sm">
|
<div className="bg-muted border-subtle absolute left-1/2 top-40 -mt-10 w-max -translate-x-1/2 -translate-y-1/2 transform rounded-md border p-8 shadow-sm">
|
||||||
<h4 className="text-emphasis mb-4 font-medium">{t("no_availability_in_month", { month: month })}</h4>
|
<h4 className="text-emphasis mb-4 font-medium">{t("no_availability_in_month", { month: month })}</h4>
|
||||||
<Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight}>
|
<Button onClick={nextMonthButton} color="primary" EndIcon={ArrowRight} data-testid="view_next_month">
|
||||||
{t("view_next_month")}
|
{t("view_next_month")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
||||||
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
|
|
||||||
|
@ -63,5 +64,22 @@ describe("Test Suite: Date Picker", () => {
|
||||||
vi.setSystemTime(vi.getRealSystemTime());
|
vi.setSystemTime(vi.getRealSystemTime());
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("it returns the correct responses end of month", () => {
|
||||||
|
// test a date at one minute past midnight, end of month.
|
||||||
|
// we use dayjs() as the system timezone can still modify the Date.
|
||||||
|
vi.useFakeTimers().setSystemTime(dayjs().endOf("month").startOf("day").add(1, "second").toDate());
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const result = getAvailableDatesInMonth({
|
||||||
|
browsingDate: currentDate,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
|
||||||
|
// Undo the forced time we applied earlier, reset to system default.
|
||||||
|
vi.setSystemTime(vi.getRealSystemTime());
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
||||||
|
|
||||||
// calculate the available dates in the month:
|
// calculate the available dates in the month:
|
||||||
|
@ -21,7 +22,9 @@ export function getAvailableDatesInMonth({
|
||||||
);
|
);
|
||||||
for (
|
for (
|
||||||
let date = browsingDate > minDate ? browsingDate : minDate;
|
let date = browsingDate > minDate ? browsingDate : minDate;
|
||||||
date <= lastDateOfMonth;
|
// Check if date is before the last date of the month
|
||||||
|
// or is the same day, in the same month, in the same year.
|
||||||
|
date < lastDateOfMonth || dayjs(date).isSame(lastDateOfMonth, "day");
|
||||||
date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)
|
date = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1)
|
||||||
) {
|
) {
|
||||||
// intersect included dates
|
// intersect included dates
|
||||||
|
|
|
@ -194,7 +194,7 @@ const OrgAppearanceView = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border-subtle rounded-md border p-5">
|
<div className="py-5">
|
||||||
<span className="text-default text-sm">{t("only_owner_change")}</span>
|
<span className="text-default text-sm">{t("only_owner_change")}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -113,7 +113,7 @@ const OrgProfileView = () => {
|
||||||
{isOrgAdminOrOwner ? (
|
{isOrgAdminOrOwner ? (
|
||||||
<OrgProfileForm defaultValues={defaultValues} />
|
<OrgProfileForm defaultValues={defaultValues} />
|
||||||
) : (
|
) : (
|
||||||
<div className="flex">
|
<div className="border-subtle flex rounded-b-md border border-t-0 px-4 py-8 sm:px-6">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-emphasis">{t("org_name")}</Label>
|
<Label className="text-emphasis">{t("org_name")}</Label>
|
||||||
|
|
|
@ -147,7 +147,7 @@ const PaymentForm = (props: Props) => {
|
||||||
formatParams: { amount: { currency: props.payment.currency } },
|
formatParams: { amount: { currency: props.payment.currency } },
|
||||||
})}
|
})}
|
||||||
onChange={(e) => setHoldAcknowledged(e.target.checked)}
|
onChange={(e) => setHoldAcknowledged(e.target.checked)}
|
||||||
descriptionClassName="text-blue-900 font-semibold"
|
descriptionClassName="text-info font-semibold"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -174,7 +174,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
<DialogContent>
|
<DialogContent enableOverflow>
|
||||||
<DialogHeader title={t("recordings_title")} subtitle={subtitle} />
|
<DialogHeader title={t("recordings_title")} subtitle={subtitle} />
|
||||||
{roomName ? (
|
{roomName ? (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
|
|
Loading…
Reference in New Issue