Merge branch 'main' into cal/automaticOTP
commit
9b1becc7a2
|
@ -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[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
|
@ -270,11 +259,21 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
const bookingLink = buildBookingLink();
|
||||
|
||||
const title = booking.title;
|
||||
// To be used after we run query on legacy bookings
|
||||
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||
|
||||
const showRecordingsButtons =
|
||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||
const showRecordingsButtons = !!(booking.isRecorded && 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 (
|
||||
<>
|
||||
|
@ -299,7 +298,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
paymentCurrency={booking.payment[0].currency}
|
||||
/>
|
||||
)}
|
||||
{showRecordingsButtons && (
|
||||
{(showRecordingsButtons || checkForRecordingsButton) && (
|
||||
<ViewRecordingsDialog
|
||||
booking={booking}
|
||||
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||
|
@ -469,7 +468,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
</>
|
||||
) : null}
|
||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
||||
{(showRecordingsButtons || checkForRecordingsButton) && (
|
||||
<TableActions actions={showRecordingActions} />
|
||||
)}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="hidden h-full items-center md:flex">
|
||||
<RequestSentMessage />
|
||||
|
|
|
@ -62,6 +62,46 @@ const triggerWebhook = async ({
|
|||
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) {
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||
|
@ -137,12 +177,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
const isUserAttendeeOrOrganiser =
|
||||
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) {
|
||||
return res.status(403).send({
|
||||
message: "Unauthorised",
|
||||
});
|
||||
const isUserMemberOfTheTeam = checkIfUserIsPartOfTheSameTeam(
|
||||
booking?.eventType?.teamId,
|
||||
session.user.id,
|
||||
session.user.email
|
||||
);
|
||||
|
||||
if (!isUserMemberOfTheTeam) {
|
||||
return res.status(403).send({
|
||||
message: "Unauthorised",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await prisma.booking.update({
|
||||
|
@ -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" });
|
||||
} 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" });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,14 +164,13 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
<>
|
||||
<Meta title={t("password")} description={t("password_description")} borderInShellHeader={true} />
|
||||
{user && user.identityProvider !== IdentityProvider.CAL ? (
|
||||
<div>
|
||||
<div className="mt-6">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||
{t("account_managed_by_identity_provider", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="border-subtle rounded-b-xl border border-t-0 px-4 py-6 sm:px-6">
|
||||
<h2 className="font-cal text-emphasis text-lg font-medium leading-6">
|
||||
{t("account_managed_by_identity_provider", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
})}
|
||||
</h2>
|
||||
|
||||
<p className="text-subtle mt-1 text-sm">
|
||||
{t("account_managed_by_identity_provider_description", {
|
||||
provider: identityProviderNameMap[user.identityProvider],
|
||||
|
@ -180,7 +179,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||
</div>
|
||||
) : (
|
||||
<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 && (
|
||||
<div className="pb-6">
|
||||
<Alert severity="error" message={formMethods.formState.errors.apiError?.message} />
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
|
||||
import type { createUsersFixture } from "./users";
|
||||
|
||||
const reschedulePlaceholderText = "Let others know why you need to reschedule";
|
||||
|
@ -38,6 +40,12 @@ type fillAndConfirmBookingParams = {
|
|||
|
||||
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 questionActions: QuestionActions = {
|
||||
phone: async () => {
|
||||
|
@ -114,6 +122,17 @@ export async function loginUser(users: UserFixture) {
|
|||
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) {
|
||||
return {
|
||||
goToEventType: async (eventType: string) => {
|
||||
|
@ -154,19 +173,13 @@ export function createBookingPageFixture(page: Page) {
|
|||
return eventtypePromise;
|
||||
},
|
||||
selectTimeSlot: async (eventTypePage: Page) => {
|
||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
||||
}
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByTestId("time").first().click();
|
||||
},
|
||||
clickReschedule: async () => {
|
||||
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 () => {
|
||||
await page.getByTestId("time").first().click();
|
||||
},
|
||||
|
@ -186,6 +199,7 @@ export function createBookingPageFixture(page: Page) {
|
|||
},
|
||||
|
||||
rescheduleBooking: async (eventTypePage: Page) => {
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByText("Reschedule").click();
|
||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
||||
|
|
|
@ -1296,6 +1296,7 @@
|
|||
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
|
||||
"check_for_conflicts": "Check for conflicts",
|
||||
"view_recordings": "View recordings",
|
||||
"check_for_recordings":"Check for recordings",
|
||||
"adding_events_to": "Adding events to",
|
||||
"follow_system_preferences": "Follow system preferences",
|
||||
"custom_brand_colors": "Custom brand colors",
|
||||
|
|
|
@ -50,8 +50,13 @@ export default class OrganizerDailyVideoDownloadRecordingEmail extends BaseEmail
|
|||
return this.getFormattedRecipientTime({ time: this.calEvent.endTime, format });
|
||||
}
|
||||
|
||||
protected getLocale(): string {
|
||||
return this.calEvent.organizer.language.locale;
|
||||
}
|
||||
|
||||
protected getFormattedDate() {
|
||||
const organizerTimeFormat = this.calEvent.organizer.timeFormat || TimeFormat.TWELVE_HOUR;
|
||||
|
||||
return `${this.getOrganizerStart(organizerTimeFormat)} - ${this.getOrganizerEnd(
|
||||
organizerTimeFormat
|
||||
)}, ${this.t(this.getOrganizerStart("dddd").toLowerCase())}, ${this.t(
|
||||
|
|
|
@ -95,7 +95,7 @@ const NoAvailabilityOverlay = ({
|
|||
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">
|
||||
<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")}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { describe, expect, test, vi } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getAvailableDatesInMonth } from "@calcom/features/calendars/lib/getAvailableDatesInMonth";
|
||||
import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
|
||||
|
||||
|
@ -63,5 +64,22 @@ describe("Test Suite: Date Picker", () => {
|
|||
vi.setSystemTime(vi.getRealSystemTime());
|
||||
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";
|
||||
|
||||
// calculate the available dates in the month:
|
||||
|
@ -21,7 +22,9 @@ export function getAvailableDatesInMonth({
|
|||
);
|
||||
for (
|
||||
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)
|
||||
) {
|
||||
// intersect included dates
|
||||
|
|
|
@ -194,7 +194,7 @@ const OrgAppearanceView = ({
|
|||
/>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -113,7 +113,7 @@ const OrgProfileView = () => {
|
|||
{isOrgAdminOrOwner ? (
|
||||
<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>
|
||||
<Label className="text-emphasis">{t("org_name")}</Label>
|
||||
|
|
|
@ -147,7 +147,7 @@ const PaymentForm = (props: Props) => {
|
|||
formatParams: { amount: { currency: props.payment.currency } },
|
||||
})}
|
||||
onChange={(e) => setHoldAcknowledged(e.target.checked)}
|
||||
descriptionClassName="text-blue-900 font-semibold"
|
||||
descriptionClassName="text-info font-semibold"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -174,7 +174,7 @@ export const ViewRecordingsDialog = (props: IViewRecordingsDialog) => {
|
|||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||
<DialogContent>
|
||||
<DialogContent enableOverflow>
|
||||
<DialogHeader title={t("recordings_title")} subtitle={subtitle} />
|
||||
{roomName ? (
|
||||
<LicenseRequired>
|
||||
|
|
Loading…
Reference in New Issue