Compare commits

..
37 Commits
Author SHA1 Message Date
Alex van Andel 4540c79580 v3.2.4
release.yml / release (push) Canceled after 0s
2023-08-25 15:32:55 +01:00
Udit Takkar 00d01882d4 fix: private team regression (#10914)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-08-25 14:43:48 +01:00
Omar López 6ce4122528 fix: allows patching booking and period limits from API (#10935) 2023-08-25 14:29:04 +01:00
7ddbcfcdcf feat: Adds filter-by-email on /users/_get endpoint for admin calls (#10928)
* adds filter by email on users get for admins

* swagger update

* Update apps/api/pages/api/users/_get.ts

Co-authored-by: Omar López <zomars@me.com>

* .parse is required, also fixed some other issues

---------

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-08-25 14:15:13 +01:00
b1b01ed383 remove close button from TopBanner (#10705)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-25 15:50:39 +03:00
6f5d9dc41f test: Create unit tests for react components in packages/ui/components/form/select (#10459)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 12:18:53 +00:00
GitStart-Cal.comandgitstart-calcom ac190b89dd chore: add UnpublishedEntity in storybook (CALCOM-10760) (#10864)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-25 15:12:19 +03:00
990441295c chore: add ImageUploader in storybook (CALCOM-10760) (#10865)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 15:06:55 +03:00
1e4c20073c chore: fix all @typescript-eslint/no-non-null-assertion warnings (#10635)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 14:56:20 +03:00
80353d67a0 chore: fix all @typescript-eslint/no-unused-vars warnings (#10645)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2023-08-25 11:48:24 +00:00
Keith Williams d55b4b051a perf: Fix slowest db query (again) (#10954) 2023-08-25 14:27:37 +03:00
Hariom Balhara 25d90e3e12 test: Upcoming bookings sorting order (#10950)
* Fix order by for bookings array
Fix orderBy for bookings

* Test

* Text change

* Add comments
2023-08-25 12:55:41 +03:00
Hariom Balhara 1727eb5625 fix: Unordered bookings (#10943) 2023-08-25 15:06:57 +05:30
sean-brydon 58f4224c66 Fix invite flow (#10951) 2023-08-25 09:19:03 +00:00
Udit Takkar 5d491e0a12 fix: unauthorized after email change (#10821) 2023-08-24 16:47:34 -07:00
alannncandOmar López 1eef5a56c3 fix: include orgs slug in getUserByUsername just like [user]/[type] page (#10723)
Co-authored-by: Omar López <zomars@me.com>
2023-08-24 16:41:06 -07:00
d6740503be feat: Column View Available Slots Fix (#9815)
Co-authored-by: Vinoth Kumar V <vinoth_kumar_v@Vinoths-MacBook-Pro.local>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2023-08-24 16:39:45 -07:00
David Oksman 9f647ead46 fix: missing timezones in dropdown list (#10823) 2023-08-24 13:57:11 -07:00
Thibault Mathian f480afcace fix: caching and fetching of feature flags (#10837) 2023-08-24 13:43:52 -07:00
Hariom Balhara 8d73d15b8a perf: Lazy load framer motion stuff as much as we can (#10876) 2023-08-24 13:40:01 -07:00
Hariom Balhara e8370ef950 perf: Remove non booker apps from booker page (#10905) 2023-08-24 13:38:15 -07:00
Hariom Balhara e859c8dc58 test: Add attendee phone number test (#10932) 2023-08-24 11:21:49 -07:00
Keith Williams b4b38b9bc3 v3.2.3
release.yml / release (push) Canceled after 0s
2023-08-24 17:16:02 +03:00
Crowdin Bot d32719ad8b New Crowdin translations by Github Action 2023-08-24 12:47:12 +00:00
Hariom Balhara 99d0816d9f phoneSchema missing async parsing (#10929) 2023-08-24 12:43:52 +00:00
95f4b89fe8 fix: #10783 (UI updated on API Creation Form ) (#10786)
* Fix #10783

* Update the changes #10783

* Updated the changes on the another dialog footer

* Updated the changes

---------

Co-authored-by: Hem Pun <hemthegentlmen@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-08-24 11:20:48 +01:00
Abhijeet SinghandAbhijeet Singh 3e63c2b717 fix: Italics not being shown in the description after saving (#10848)
* added ignore rule for em tag in turndown

* added ignore rule for em tag in turndown

---------

Co-authored-by: Abhijeet Singh <asing9829@gmail.com>
2023-08-24 11:20:00 +01:00
Omar López 42cd56578d fix: Reduced boilerplate for publicViewer router (#10849) 2023-08-24 11:19:24 +01:00
Ujwal KumarandBailey Pumfleet 31d4cb4eb7 Transform # character to - in slug (#10896)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-08-24 11:19:10 +01:00
sean-brydonandBailey Pumfleet fde3e8bea9 Invalidate and add success message (#10907)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2023-08-24 11:18:59 +01:00
GitStart-Cal.comandgitstart-calcom 874ad29676 chore: add Table in storybook (CALCOM-10760) (#10869)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-24 11:07:57 +01:00
Hariom Balhara 5503d9d432 perf: bookings query improvement (#10687) 2023-08-24 11:14:10 +02:00
Keith WilliamsandShivam Kalra 43b3d68447 chore: Don't render certain components when running meticulous tests (#10772)
* chore: Don't render credits when running meticulous tests

* fix: type window Meticulous

* fix: window reference error

* chore: window interface

* chore: disable tzdialog popup on meticulous

* chore: IS_VISUAL_REGRESSION_TESTING

* fix: constants

* fix: only run on preview builds

---------

Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-08-24 13:42:53 +05:30
GitStart-Cal.comandgitstart-calcom e3c747a867 test: Create unit tests for react components is packages/ui/components/navigation (#10698)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
2023-08-24 13:23:54 +05:30
sean-brydonandOmar López e020a2c9a4 feat: Availability Slider enabled for teams (#10902)
Co-authored-by: Omar López <zomars@me.com>
2023-08-23 21:23:24 +00:00
Leo Giovanetti e6e6f09547 feat: admin org list now shows all, dns check added (#10875)
* chore: admin org list now shows all, dns check added

* Updating email template

* type fixes
2023-08-23 21:01:12 +00:00
Leo GiovanettiandJoe Au-Yeung 68ec946c59 fix: add org avatar to org booking pages [CAL-2333] (#10847)
* Add org avatar to org booking pages

* Using web app url for org avatars + missing i18n string

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-08-23 20:10:02 +00:00
170 changed files with 3092 additions and 985 deletions
+7 -1
View File
@@ -33,12 +33,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
position: true,
eventName: true,
timeZone: true,
schedulingType: true,
// START Limit future bookings
periodType: true,
periodStartDate: true,
schedulingType: true,
periodEndDate: true,
periodDays: true,
periodCountCalendarDays: true,
// END Limit future bookings
requiresConfirmation: true,
disableGuests: true,
hideCalendarNotes: true,
@@ -51,6 +53,8 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
slotInterval: true,
successRedirectUrl: true,
locations: true,
bookingLimits: true,
durationLimits: true,
})
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
.partial()
@@ -126,6 +130,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
seatsPerTimeSlot: true,
seatsShowAttendees: true,
bookingFields: true,
bookingLimits: true,
durationLimits: true,
}).merge(
z.object({
locations: z
@@ -0,0 +1,20 @@
import { withValidation } from "next-validations";
import { z } from "zod";
import { baseApiParams } from "./baseApiParams";
// Extracted out as utility function so can be reused
// at different endpoints that require this validation.
export const schemaQueryUserEmail = baseApiParams.extend({
email: z.string().email(),
});
export const schemaQuerySingleOrMultipleUserEmails = z.object({
email: z.union([z.string().email(), z.array(z.string().email())]),
});
export const withValidQueryUserEmail = withValidation({
schema: schemaQueryUserEmail,
type: "Zod",
mode: "query",
});
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import type { z } from "zod";
@@ -202,10 +202,17 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query);
const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body);
const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeEditBodyParams.parse(body);
const data: Prisma.EventTypeUpdateArgs["data"] = {
...parsedBody,
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
};
if (hosts) {
+9 -2
View File
@@ -1,4 +1,4 @@
import type { Prisma } from "@prisma/client";
import { Prisma } from "@prisma/client";
import type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error";
@@ -259,12 +259,19 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
async function postHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma, body } = req;
const { hosts = [], ...parsedBody } = schemaEventTypeCreateBodyParams.parse(body || {});
const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeCreateBodyParams.parse(body || {});
let data: Prisma.EventTypeCreateArgs["data"] = {
...parsedBody,
userId,
users: { connect: { id: userId } },
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
};
await checkPermissions(req);
+20
View File
@@ -4,6 +4,7 @@ import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware";
import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail";
import { schemaUsersReadPublic } from "~/lib/validations/user";
/**
@@ -19,6 +20,17 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
* schema:
* type: string
* description: Your API key
* - in: query
* name: email
* required: false
* schema:
* type: array
* items:
* type: string
* format: email
* style: form
* explode: true
* description: The email address or an array of email addresses to filter by
* tags:
* - users
* responses:
@@ -39,6 +51,14 @@ export async function getHandler(req: NextApiRequest) {
const where: Prisma.UserWhereInput = {};
// If user is not ADMIN, return only his data.
if (!isAdmin) where.id = userId;
if (req.query.email) {
const validationResult = schemaQuerySingleOrMultipleUserEmails.parse(req.query);
where.email = {
in: Array.isArray(validationResult.email) ? validationResult.email : [validationResult.email],
};
}
const [total, data] = await prisma.$transaction([
prisma.user.count({ where }),
prisma.user.findMany({ where, take, skip }),
+21 -1
View File
@@ -1,9 +1,29 @@
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
const ns = ["common"];
const supportedLngs = ["en", "fr"];
const resources = ns.reduce((acc, n) => {
supportedLngs.forEach((lng) => {
if (!acc[lng]) acc[lng] = {};
acc[lng] = {
...acc[lng],
[n]: require(`../../web/public/static/locales/${lng}/${n}.json`),
};
});
return acc;
}, {});
i18n.use(initReactI18next).init({
resources: [],
debug: true,
fallbackLng: "en",
defaultNS: "common",
ns,
interpolation: {
escapeValue: false,
},
react: { useSuspense: true },
resources,
});
export default i18n;
+8
View File
@@ -21,6 +21,14 @@ export const parameters = {
push() {},
Provider: AppRouterContext.Provider,
},
globals: {
locale: "en",
locales: {
en: "English",
fr: "Français",
},
},
i18n,
};
addDecorator((storyFn) => (
+2 -1
View File
@@ -18,6 +18,7 @@ export function VariantsTable({
const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
return (
<div
id="light-variant"
className={classNames(
isDark &&
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]"
@@ -43,7 +44,7 @@ export function VariantsTable({
</table>
</div>
{!isDark && (
<div data-mode="dark" className="dark">
<div id="dark-variant" data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children}
</VariantsTable>
@@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="hover:bg-muted group flex flex-col sm:flex-row">
<tr data-testid="booking-item" className="hover:bg-muted group flex flex-col sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
@@ -35,6 +35,7 @@ import {
Tooltip,
} from "@calcom/ui";
import { Copy, Edit } from "@calcom/ui/components/icon";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -286,36 +287,38 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
<TextField
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
labelSrOnly
type="text"
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
/>
{!IS_VISUAL_REGRESSION_TESTING && (
<TextField
disabled
name="hashedLink"
label={t("private_link_label")}
data-testid="generated-hash-url"
labelSrOnly
type="text"
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
addOnSuffix={
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
<Button
color="minimal"
size="sm"
type="button"
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
}
}}>
<Copy className="h-4 w-4" />
</Button>
</Tooltip>
}
/>
)}
</div>
</SettingsToggle>
<hr className="border-subtle" />
@@ -22,7 +22,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId },
{ scheduleId: defaultScheduleId ?? undefined },
{
enabled: !!scheduleId,
}
+1
View File
@@ -0,0 +1 @@
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.2",
"version": "3.2.4",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1
View File
@@ -75,6 +75,7 @@ export default function Custom404() {
)}`
);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const isSuccessPage = pathname?.startsWith("/booking");
-1
View File
@@ -53,7 +53,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
orgSlug: _orgSlug,
...query
} = useRouterQuery();
const nameOrUsername = user.name || user.username || "";
/*
const telemetry = useTelemetry();
+1 -1
View File
@@ -51,7 +51,7 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" />
{(!IS_PRODUCTION || process.env.VERCEL_ENV === "preview") && (
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
// eslint-disable-next-line @next/next/no-sync-scripts
<script
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
+6 -16
View File
@@ -39,19 +39,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const username = slugify(data.username);
const userEmail = email.toLowerCase();
const validationResponse = (
incomingEmail: string,
validation: { isValid: boolean; email: string | undefined }
) => {
const { isValid, email } = validation;
if (!isValid) {
const message: string =
email !== incomingEmail ? "Username already taken" : "Email address is already registered";
return res.status(409).json({ message });
}
};
if (!username) {
res.status(422).json({ message: "Invalid username" });
return;
@@ -79,11 +66,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
if (foundToken?.teamId) {
const teamUserValidation = await validateUsernameInTeam(username, userEmail, foundToken?.teamId);
return validationResponse(userEmail, teamUserValidation);
if (!teamUserValidation.isValid) {
return res.status(409).json({ message: "Username or email is already taken" });
}
}
} else {
const userValidation = await validateUsername(username, userEmail);
return validationResponse(userEmail, userValidation);
if (!userValidation.isValid) {
return res.status(409).json({ message: "Username or email is already taken" });
}
}
const hashedPassword = await hashPassword(password);
@@ -94,7 +85,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: foundToken.teamId,
},
});
if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
-48
View File
@@ -7,54 +7,6 @@ import { getTranslation } from "@calcom/lib/server/i18n";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common");
const language = { translate: t, locale: "en" };
const evt = {
type: "30min",
title: "30min between Pro Example and pro@example.com",
description: null,
additionalNotes: "asdasdas",
startTime: "2022-06-03T09:00:00-06:00",
endTime: "2022-06-03T09:30:00-06:00",
organizer: {
name: "Pro Example",
email: "pro@example.com",
timeZone: "Europe/London",
language,
},
attendees: [
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
],
location: "Zoom video",
destinationCalendar: null,
hideCalendarNotes: false,
uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {},
recurringEvent: null,
appsStatus: [
{
appName: "Outlook Calendar",
type: "office365_calendar",
success: 1,
failures: 0,
errors: [],
warnings: [],
},
{
appName: "Google Meet",
type: "conferencing",
success: 0,
failures: 1,
errors: [],
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
},
],
};
res.statusCode = 200;
+13 -16
View File
@@ -2,7 +2,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useCallback } from "react";
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { ShellMain } from "@calcom/features/shell/Shell";
@@ -135,7 +134,6 @@ export default function AvailabilityPage() {
const searchParams = useSearchParams();
const router = useRouter();
const pathname = usePathname();
const orgBranding = useOrgBranding();
// Get a new searchParams string by merging the current
// searchParams with a provided key/value pair
@@ -156,23 +154,22 @@ export default function AvailabilityPage() {
subtitle={t("configure_availability")}
CTA={
<div className="flex gap-2">
{orgBranding && (
<ToggleGroup
className="hidden md:block"
defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => {
router.push(`${pathname}?${createQueryString("type", value)}`);
}}
options={[
{ value: "mine", label: t("my_availability") },
{ value: "team", label: t("team_availability") },
]}
/>
)}
<ToggleGroup
className="hidden md:block"
defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => {
if (!value) return;
router.push(`${pathname}?${createQueryString("type", value)}`);
}}
options={[
{ value: "mine", label: t("my_availability") },
{ value: "team", label: t("team_availability") },
]}
/>
<NewScheduleButton />
</div>
}>
{searchParams?.get("type") === "team" && orgBranding ? (
{searchParams?.get("type") === "team" ? (
<AvailabilitySliderTable />
) : (
<WithQuery
+1 -1
View File
@@ -179,7 +179,7 @@ export default function Bookings() {
)}
<div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border">
<table className="w-full max-w-full table-fixed">
<table data-testid={`${status}-bookings`} className="w-full max-w-full table-fixed">
<tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => (
<Fragment key={index}>
@@ -411,6 +411,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
throw new Error(t("seats_and_no_show_fee_error"));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
@@ -495,6 +496,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
updateMutation.mutate({
...rest,
@@ -1,9 +1,9 @@
import UnverifiedOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/unverifiedOrgPage";
import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
import type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper";
const Page = UnverifiedOrgsPage as CalPageWrapper;
const Page = AdminOrgsPage as CalPageWrapper;
Page.PageWrapper = PageWrapper;
export default Page;
+17 -15
View File
@@ -77,26 +77,25 @@ type FormValues = {
const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: _session, update } = useSession();
const { update } = useSession();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (res) => {
await update(res);
showToast(t("settings_updated_successfully"), "success");
if (res.signOutUser && tempFormValues) {
if (res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
} else {
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
}
// signout user only in case of password reset
if (res.signOutUser && tempFormValues && res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
await signOut({ callbackUrl: "/auth/logout?passReset=true" });
} else {
utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate();
utils.viewer.shouldVerifyEmail.invalidate();
}
utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate();
setConfirmAuthEmailChangeWarningDialogOpen(false);
update(res);
setTempFormValues(null);
},
onError: () => {
@@ -325,7 +324,10 @@ const ProfileView = () => {
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
</div>
<DialogFooter showDivider>
<Button color="primary" onClick={(e) => onConfirmPassword(e)}>
<Button
color="primary"
loading={confirmPasswordMutation.isLoading}
onClick={(e) => onConfirmPassword(e)}>
{t("confirm")}
</Button>
<DialogClose />
@@ -345,7 +347,7 @@ const ProfileView = () => {
<DialogFooter>
<Button
color="primary"
disabled={updateProfileMutation.isLoading}
loading={updateProfileMutation.isLoading}
onClick={(e) => onConfirmAuthEmailChange(e)}>
{t("confirm")}
</Button>
+11 -3
View File
@@ -199,19 +199,27 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<SubTeams />
) : (
<>
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{(showMembers.isOn || !team.eventTypes.length) &&
(team.isPrivate ? (
<div className="w-full text-center">
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_team_members")}</h2>
</div>
) : (
<Team team={team} />
))}
{!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<EventTypes />
{!team.hideBookATeamMember && (
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{(!team.hideBookATeamMember || team.isPrivate) && (
<div>
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" />
</div>
<div className="relative flex justify-center">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
<span className="dark:bg-darkgray-50 bg-subtle text-subtle px-2 text-sm">
{t("or")}
</span>
</div>
+113
View File
@@ -0,0 +1,113 @@
import { expect } from "@playwright/test";
import { BookingStatus } from "@calcom/prisma/client";
import type { Fixtures } from "./lib/fixtures";
import { test } from "./lib/fixtures";
test.afterEach(({ users }) => users.deleteAll());
test.describe("Bookings", () => {
test.describe("Upcoming bookings", () => {
test("show attendee bookings and organizer bookings in asc order by startDate", async ({
page,
users,
bookings,
}) => {
const firstUser = await users.create();
const secondUser = await users.create();
const bookingWhereFirstUserIsOrganizerFixture = await createBooking({
title: "Booking as organizer",
bookingsFixture: bookings,
// Create a booking 3 days from today
relativeDate: 3,
organizer: firstUser,
organizerEventType: firstUser.eventTypes[0],
attendees: [
{ name: "First", email: "first@cal.com", timeZone: "Europe/Berlin" },
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
],
});
const bookingWhereFirstUserIsOrganizer = await bookingWhereFirstUserIsOrganizerFixture.self();
const bookingWhereFirstUserIsAttendeeFixture = await createBooking({
title: "Booking as attendee",
bookingsFixture: bookings,
organizer: secondUser,
// Booking created 2 days from today
relativeDate: 2,
organizerEventType: secondUser.eventTypes[0],
attendees: [
{ name: "OrganizerAsBooker", email: firstUser.email, timeZone: "Europe/Berlin" },
{ name: "Second", email: "second@cal.com", timeZone: "Europe/Berlin" },
{ name: "Third", email: "third@cal.com", timeZone: "Europe/Berlin" },
],
});
const bookingWhereFirstUserIsAttendee = await bookingWhereFirstUserIsAttendeeFixture.self();
await firstUser.apiLogin();
await page.goto(`/bookings/upcoming`);
const upcomingBookings = page.locator('[data-testid="upcoming-bookings"]');
const firstUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(0);
const secondUpcomingBooking = upcomingBookings.locator('[data-testid="booking-item"]').nth(1);
await expect(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
firstUpcomingBooking.locator(`text=${bookingWhereFirstUserIsAttendee!.title}`)
).toBeVisible();
await expect(
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
).toBeVisible();
});
});
});
async function createBooking({
bookingsFixture,
organizer,
organizerEventType,
attendees,
/**
* Relative date from today
* 0 means today
* 1 means tomorrow
*/
relativeDate = 0,
durationMins = 30,
title,
}: {
bookingsFixture: Fixtures["bookings"];
organizer: {
id: number;
username: string | null;
};
organizerEventType: {
id: number;
};
attendees: {
name: string;
email: string;
timeZone: string;
}[];
relativeDate?: number;
durationMins?: number;
title: string;
}) {
const DAY_MS = 24 * 60 * 60 * 1000;
const bookingDurationMs = durationMins * 60 * 1000;
const startTime = new Date(Date.now() + relativeDate * DAY_MS);
const endTime = new Date(Date.now() + relativeDate * DAY_MS + bookingDurationMs);
return await bookingsFixture.create(organizer.id, organizer.username, organizerEventType.id, {
title,
status: BookingStatus.ACCEPTED,
startTime,
endTime,
attendees: {
createMany: {
data: [...attendees],
},
},
});
}
+43
View File
@@ -1,3 +1,4 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -91,6 +92,7 @@ test.describe("Event Types tests", () => {
expect(formTitle).toBe(firstTitle);
expect(formSlug).toContain(firstSlug);
});
test("edit first event", async ({ page }) => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
@@ -152,5 +154,46 @@ test.describe("Event Types tests", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test.describe("Different Locations Tests", () => {
test("can add Attendee Phone Number location and book with it", async ({ page }) => {
await gotoFirstEventType(page);
await selectAttendeePhoneNumber(page);
await saveEventType(page);
await gotoBookingPage(page);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator(`[data-fob-field-name="location"] input`).fill("9199999999");
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
await expect(page.locator("text=+19199999999")).toBeVisible();
});
});
});
});
const selectAttendeePhoneNumber = async (page: Page) => {
const locationOptionText = "Attendee Phone Number";
await page.locator("#location-select").click();
await page.locator(`text=${locationOptionText}`).click();
};
async function gotoFirstEventType(page: Page) {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first();
await firstEventTypeElement.click();
await page.waitForURL((url) => {
return !!url.pathname.match(/\/event-types\/.+/);
});
}
async function saveEventType(page: Page) {
await page.locator("[data-testid=update-eventtype]").click();
}
async function gotoBookingPage(page: Page) {
const previewLink = await page.locator("[data-testid=preview-button]").getAttribute("href");
await page.goto(previewLink ?? "");
}
+6 -3
View File
@@ -19,9 +19,12 @@ export const createBookingsFixture = (page: Page) => {
username: string | null,
eventTypeId = -1,
{
title = "",
rescheduled = false,
paid = false,
status = "ACCEPTED",
startTime,
endTime,
attendees = {
create: {
email: "attendee@example.com",
@@ -39,9 +42,9 @@ export const createBookingsFixture = (page: Page) => {
const booking = await prisma.booking.create({
data: {
uid: uid,
title: "30min",
startTime: startDate,
endTime: endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
title: title || "30min",
startTime: startTime || startDate,
endTime: endTime || endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
user: {
connect: {
id: userId,
+1 -1
View File
@@ -542,7 +542,7 @@ export async function apiLogin(
.then((json) => json.csrfToken);
const data = {
email: user.email ?? `${user.username}@example.com`,
password: user.password ?? user.username!,
password: user.password ?? user.username,
callbackURL: "http://localhost:3000/",
redirect: "false",
json: "true",
@@ -2010,6 +2010,21 @@
"member_removed": "Member removed",
"my_availability": "My Availability",
"team_availability": "Team Availability",
"admin_org_notification_email_subject": "New organization created: pending action",
"hi_admin": "Hi Administrator",
"admin_org_notification_email_title": "An organization requires DNS setup",
"admin_org_notification_email_body_part1": "An organization with slug \"{{orgSlug}}\" was created.<br /><br />Please be sure to configure your DNS registry to point the subdomain corresponding to the new organization to where the main app is running. Otherwise the organization will not work.<br /><br />Here are just the very basic options to configure a subdomain to point to their app so it loads the organization profile page.<br /><br />You can do it either with the A Record:",
"admin_org_notification_email_body_part2": "Or the CNAME record:",
"admin_org_notification_email_body_part3": "Once you configure the subdomain, please mark the DNS configuration as done in Organizations Admin Settings.",
"admin_org_notification_email_cta": "Go to Organizations Admin Settings",
"org_has_been_processed": "Org has been processed",
"org_error_processing": "There has been an error processing this organization",
"orgs_page_description": "A list of all organizations. Accepting an organization will allow all users with that email domain to sign up WITHOUT email verifciation.",
"unverified": "Unverified",
"dns_missing": "DNS Missing",
"mark_dns_configured": "Mark as DNS configured",
"value": "Value",
"your_organization_updated_sucessfully": "Your organization updated successfully",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -2006,5 +2006,17 @@
"member_removed": "Membre supprimé",
"my_availability": "Mes disponibilités",
"team_availability": "Disponibilités de l'équipe",
"admin_org_notification_email_subject": "Nouvelle organisation créée : action en attente",
"admin_org_notification_email_title": "Une organisation nécessite une configuration DNS",
"admin_org_notification_email_body_part2": "Ou l'enregistrement CNAME :",
"admin_org_notification_email_body_part3": "Une fois le sous-domaine configuré, veuillez marquer la configuration DNS comme terminée dans les paramètres d'administration des organisations.",
"admin_org_notification_email_cta": "Accéder aux paramètres d'administration des organisations",
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
"unverified": "Non vérifié",
"dns_missing": "DNS manquant",
"mark_dns_configured": "Marquer comme DNS configuré",
"value": "Valeur",
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
+129 -97
View File
@@ -269,12 +269,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
},
});
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
@@ -353,13 +355,15 @@ describe("getSchedule", () => {
// Day Plus 2 is completely free - It only has non accepted bookings
const scheduleOnCompletelyFreeDay = await getSchedule({
eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
// getSchedule returns timeslots in GMT
@@ -384,12 +388,14 @@ describe("getSchedule", () => {
// Day plus 3
const scheduleForDayWithOneBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
@@ -448,12 +454,14 @@ describe("getSchedule", () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scheduleForEventWith30Length = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForEventWith30Length).toHaveTimeSlots(
@@ -482,12 +490,14 @@ describe("getSchedule", () => {
);
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
// `slotInterval` takes precedence over `length`
// 4:30 is utc so it is 10:00 in IST
@@ -545,12 +555,14 @@ describe("getSchedule", () => {
const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
[
@@ -564,12 +576,14 @@ describe("getSchedule", () => {
);
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
[
@@ -627,12 +641,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
@@ -700,12 +716,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const scheduleForEventOnADayWithCalBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
@@ -757,12 +775,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const schedule = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(schedule).toHaveTimeSlots(
@@ -820,12 +840,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData);
const scheduleForEventOnADayWithDateOverride = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
@@ -905,12 +927,14 @@ describe("getSchedule", () => {
// Requesting this user's availability for their
// individual Event Type
const thisUserAvailability = await getSchedule({
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
input: {
eventTypeId: 2,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: false,
}
});
expect(thisUserAvailability).toHaveTimeSlots(
@@ -1002,12 +1026,14 @@ describe("getSchedule", () => {
});
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
});
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
@@ -1030,12 +1056,14 @@ describe("getSchedule", () => {
);
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
});
// A user with blocked time in another event, still affects Team Event availability
@@ -1137,12 +1165,14 @@ describe("getSchedule", () => {
hosts: [],
});
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
@@ -1163,12 +1193,14 @@ describe("getSchedule", () => {
);
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
input: {
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
isTeamEvent: true,
}
});
// A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
@@ -303,6 +303,7 @@ describe("handleChildrenEventTypes", () => {
timeZone: _timeZone,
parentId: _parentId,
userId: _userId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requiresBookerEmailVerification,
...evType
} = mockFindFirstEventType({
+44 -6
View File
@@ -40,6 +40,7 @@ type App = Partial<AppMeta> & {
function generateFiles() {
const browserOutput = [`import dynamic from "next/dynamic"`];
const metadataOutput = [];
const bookerMetadataOutput = [];
const schemasOutput = [];
const appKeysSchemasOutput = [];
const serverOutput = [];
@@ -70,22 +71,30 @@ function generateFiles() {
}
});
function forEachAppDir(callback: (arg: App) => void) {
function forEachAppDir(callback: (arg: App) => void, filter: (arg: App) => boolean = () => true) {
for (let i = 0; i < appDirs.length; i++) {
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json");
const metadataPath = path.join(APP_STORE_PATH, appDirs[i].path, "_metadata.ts");
let app;
if (fs.existsSync(configPath)) {
app = JSON.parse(fs.readFileSync(configPath).toString());
} else if (fs.existsSync(metadataPath)) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
app = require(metadataPath).metadata;
} else {
app = {};
}
callback({
const finalApp = {
...app,
name: appDirs[i].name,
path: appDirs[i].path,
});
};
if (filter(finalApp)) {
callback(finalApp);
}
}
}
@@ -130,7 +139,8 @@ function generateFiles() {
lazyImport?: boolean;
importConfig: ImportConfig;
entryObjectKeyGetter?: (arg: App, importName?: string) => string;
}
},
filter?: (arg: App) => boolean
) {
const output: string[] = [];
@@ -174,7 +184,7 @@ function generateFiles() {
}
}
}
});
}, filter);
}
function createExportObject() {
@@ -201,7 +211,7 @@ function generateFiles() {
}
}
}
});
}, filter);
output.push(`};`);
}
@@ -247,6 +257,25 @@ function generateFiles() {
})
);
bookerMetadataOutput.push(
...getExportedObject(
"appStoreMetadata",
{
// Try looking for config.json and if it's not found use _metadata.ts to generate appStoreMetadata
importConfig: [
{
fileToBeImported: "config.json",
importName: "default",
},
{
fileToBeImported: "_metadata.ts",
importName: "metadata",
},
],
},
isBookerApp
)
);
schemasOutput.push(
...getExportedObject("appDataSchemas", {
// Import path must have / even for windows and not \
@@ -312,6 +341,7 @@ function generateFiles() {
["apps.browser.generated.tsx", browserOutput],
["apps.schemas.generated.ts", schemasOutput],
["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
["bookerApps.metadata.generated.ts", bookerMetadataOutput],
];
filesToGenerate.forEach(([fileName, output]) => {
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
@@ -347,3 +377,11 @@ if (isInWatchMode) {
} else {
generateFiles();
}
function isBookerApp(app: App) {
// Right now there are only two types of Apps that booker needs.
// Note that currently payment apps' meta don't need to be accessed on booker. We just access from DB eventType.metadata
// 1. It is a location app(e.g. any Conferencing App)
// 2. It is a tag manager app(e.g. Google Analytics, GTM, Fathom)
return !!(app.appData?.location || app.appData?.tag);
}
+1 -1
View File
@@ -1,7 +1,7 @@
import Script from "next/script";
import { getEventTypeAppData } from "@calcom/app-store/_utils/getEventTypeAppData";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData";
import type { appDataSchemas } from "./apps.schemas.generated";
+2 -15
View File
@@ -1,7 +1,7 @@
import type { AppMeta } from "@calcom/types/App";
import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated";
import { getAppAssetFullPath } from "./getAppAssetFullPath";
import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata";
type RawAppStoreMetaData = typeof rawAppStoreMetadata;
type AppStoreMetaData = {
@@ -10,18 +10,5 @@ type AppStoreMetaData = {
export const appStoreMetadata = {} as AppStoreMetaData;
for (const [key, value] of Object.entries(rawAppStoreMetadata)) {
const dirName = "dirName" in value ? value.dirName : value.slug;
if (!dirName) {
throw new Error(`Couldn't derive dirName for app ${key}`);
}
const metadata = (appStoreMetadata[key as keyof typeof appStoreMetadata] = {
appData: null,
dirName,
__template: "",
...value,
} as AppStoreMetaData[keyof AppStoreMetaData]);
metadata.logo = getAppAssetFullPath(metadata.logo, {
dirName,
isTemplate: metadata.isTemplate,
});
appStoreMetadata[key as keyof typeof appStoreMetadata] = getNormalizedAppMetadata(value);
}
@@ -0,0 +1,67 @@
/**
This file is autogenerated using the command `yarn app-store:build --watch`.
Don't modify this file manually.
**/
import around_config_json from "./around/config.json";
import campfire_config_json from "./campfire/config.json";
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
import discord_config_json from "./discord/config.json";
import eightxeight_config_json from "./eightxeight/config.json";
import element_call_config_json from "./element-call/config.json";
import facetime_config_json from "./facetime/config.json";
import fathom_config_json from "./fathom/config.json";
import ga4_config_json from "./ga4/config.json";
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
import gtm_config_json from "./gtm/config.json";
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import office365video_config_json from "./office365video/config.json";
import ping_config_json from "./ping/config.json";
import plausible_config_json from "./plausible/config.json";
import riverside_config_json from "./riverside/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import sylapsvideo_config_json from "./sylapsvideo/config.json";
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
import telegram_config_json from "./telegram/config.json";
import booking_pages_tag_config_json from "./templates/booking-pages-tag/config.json";
import event_type_location_video_static_config_json from "./templates/event-type-location-video-static/config.json";
import webex_config_json from "./webex/config.json";
import whatsapp_config_json from "./whatsapp/config.json";
import whereby_config_json from "./whereby/config.json";
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
export const appStoreMetadata = {
around: around_config_json,
campfire: campfire_config_json,
dailyvideo: dailyvideo__metadata_ts,
discord: discord_config_json,
eightxeight: eightxeight_config_json,
"element-call": element_call_config_json,
facetime: facetime_config_json,
fathom: fathom_config_json,
ga4: ga4_config_json,
googlevideo: googlevideo__metadata_ts,
gtm: gtm_config_json,
huddle01video: huddle01video__metadata_ts,
jitsivideo: jitsivideo__metadata_ts,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
office365video: office365video_config_json,
ping: ping_config_json,
plausible: plausible_config_json,
riverside: riverside_config_json,
signal: signal_config_json,
sirius_video: sirius_video_config_json,
sylapsvideo: sylapsvideo_config_json,
tandemvideo: tandemvideo__metadata_ts,
telegram: telegram_config_json,
"booking-pages-tag": booking_pages_tag_config_json,
"event-type-location-video-static": event_type_location_video_static_config_json,
webex: webex_config_json,
whatsapp: whatsapp_config_json,
whereby: whereby_config_json,
zoomvideo: zoomvideo__metadata_ts,
};
+16
View File
@@ -0,0 +1,16 @@
import type { AppMeta } from "@calcom/types/App";
// We have to import all the booker-apps config/metadata in here as without importing that we can't dig into their config and read their props
// It isn't a significant bundle-size impact as we are only importing the metadata of only the booker apps, but when it becomes a problem we can figure out a solution
import { appStoreMetadata as rawBookerAppsMetadata } from "./bookerApps.metadata.generated";
import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata";
type RawAppStoreMetaData = typeof rawBookerAppsMetadata;
type AppStoreMetaData = {
[key in keyof RawAppStoreMetaData]: AppMeta;
};
export const appStoreMetadata = {} as AppStoreMetaData;
for (const [key, value] of Object.entries(rawBookerAppsMetadata)) {
appStoreMetadata[key as keyof typeof appStoreMetadata] = getNormalizedAppMetadata(value);
}
@@ -0,0 +1,28 @@
import type { AppMeta } from "@calcom/types/App";
// We have to import all the booker-apps config/metadata in here as without that we couldn't
import type { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated";
import { getAppAssetFullPath } from "./getAppAssetFullPath";
type RawAppStoreMetaData = typeof rawAppStoreMetadata;
type AppStoreMetaData = {
[key in keyof RawAppStoreMetaData]: AppMeta;
};
export const getNormalizedAppMetadata = (appMeta: RawAppStoreMetaData[keyof RawAppStoreMetaData]) => {
const dirName = "dirName" in appMeta ? appMeta.dirName : appMeta.slug;
if (!dirName) {
throw new Error(`Couldn't derive dirName for app ${appMeta.name}`);
}
const metadata = {
appData: null,
dirName,
__template: "",
...appMeta,
} as AppStoreMetaData[keyof AppStoreMetaData];
metadata.logo = getAppAssetFullPath(metadata.logo, {
dirName,
isTemplate: metadata.isTemplate,
});
return metadata;
};
+1 -1
View File
@@ -1,7 +1,7 @@
import type { TFunction } from "next-i18next";
import { z } from "zod";
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
import { appStoreMetadata } from "@calcom/app-store/bookerAppsMetaData";
import logger from "@calcom/lib/logger";
import { BookingStatus } from "@calcom/prisma/enums";
import type { Ensure, Optional } from "@calcom/types/utils";
@@ -11,7 +11,7 @@ import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
const { t } = useLocale();
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
const [_getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
const [additionalParameters, setAdditionalParameters] = useState("");
const { enabled, updateEnabled } = useIsAppEnabled(app);
@@ -66,7 +66,6 @@ function Field({
hookForm,
hookFieldNamespace,
deleteField,
fieldIndex,
moveUp,
moveDown,
appUrl,
@@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -1,8 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import type { App_RoutingForms_Form } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { getSerializableForm } from "../lib/getSerializableForm";
@@ -1,7 +1,8 @@
import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import logger from "@calcom/lib/logger";
import type { PrismaClient, Prisma } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { Prisma } from "@calcom/prisma/client";
import { entries } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
@@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
@@ -1,7 +1,7 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { z } from "zod";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { getSerializableForm } from "../lib/getSerializableForm";
@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring";
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import type { StripeData } from "../lib/server";
import stripe from "../lib/server";
@@ -31,8 +30,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(401).json({ message: "You must be logged in to do this" });
}
const state = decodeOAuthState(req);
const response = await stripe.oauth.token({
grant_type: "authorization_code",
code: code?.toString(),
@@ -9,7 +9,6 @@ import stripe from "../lib/server";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST" && req.method !== "GET")
return res.status(405).json({ message: "Method not allowed" });
const { referer } = req.headers;
// if (!referer) return res.status(400).json({ message: "Missing referrer" });
+6
View File
@@ -9,6 +9,8 @@ import type { CalendarEvent, Person } from "@calcom/types/Calendar";
import type { EmailVerifyLink } from "./templates/account-verify-email";
import AccountVerifyEmail from "./templates/account-verify-email";
import type { OrganizationNotification } from "./templates/admin-organization-notification";
import AdminOrganizationNotification from "./templates/admin-organization-notification";
import AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-email";
@@ -376,3 +378,7 @@ export const sendDailyVideoRecordingEmails = async (calEvent: CalendarEvent, dow
export const sendOrganizationEmailVerification = async (sendOrgInput: OrganizationEmailVerify) => {
await sendEmail(() => new OrganizationEmailVerification(sendOrgInput));
};
export const sendAdminOrganizationNotification = async (input: OrganizationNotification) => {
await sendEmail(() => new AdminOrganizationNotification(input));
};
@@ -5,7 +5,6 @@ import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { CallToActionIcon } from "./CallToActionIcon";
import { Info } from "./Info";
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) {
@@ -0,0 +1,118 @@
import { Trans, type TFunction } from "next-i18next";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { BaseEmailHtml, CallToAction } from "../components";
type AdminOrganizationNotification = {
language: TFunction;
orgSlug: string;
webappIPAddress: string;
};
const dnsTable = (type: string, name: string, value: string, t: TFunction) => (
<table
role="presentation"
border={0}
cellSpacing="0"
cellPadding="0"
style={{
verticalAlign: "top",
marginTop: "10px",
borderRadius: "6px",
borderCollapse: "separate",
border: "solid black 1px",
}}
width="100%">
<tbody>
<thead>
<tr
style={{
backgroundColor: "black",
color: "white",
fontSize: "14px",
lineHeight: "24px",
}}>
<td
align="center"
width="33%"
style={{ borderTopLeftRadius: "5px", borderRight: "1px solid white" }}>
{t("type")}
</td>
<td align="center" width="33%" style={{ borderRight: "1px solid white" }}>
{t("name")}
</td>
<td align="center" style={{ borderTopRightRadius: "5px" }}>
{t("value")}
</td>
</tr>
</thead>
<tr style={{ lineHeight: "24px" }}>
<td align="center" style={{ borderBottomLeftRadius: "5px", borderRight: "1px solid black" }}>
{type}
</td>
<td align="center" style={{ borderRight: "1px solid black" }}>
{name}
</td>
<td align="center" style={{ borderBottomRightRadius: "5px" }}>
{value}
</td>
</tr>
</tbody>
</table>
);
export const AdminOrganizationNotificationEmail = ({
orgSlug,
webappIPAddress,
language,
}: AdminOrganizationNotification) => {
const webAppUrl = WEBAPP_URL.replace("https://", "")?.replace("http://", "").replace(/(:.*)/, "");
return (
<BaseEmailHtml
subject={language("admin_org_notification_email_subject", { appName: APP_NAME })}
callToAction={
<CallToAction
label={language("admin_org_notification_email_cta")}
href={`${WEBAPP_URL}/settings/admin/organizations`}
endIconName="white-arrow-right"
/>
}>
<p
style={{
fontWeight: 600,
fontSize: "24px",
lineHeight: "38px",
}}>
<>{language("admin_org_notification_email_title")}</>
</p>
<p style={{ fontWeight: 400 }}>
<>{language("hi_admin")}!</>
</p>
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
<Trans i18nKey="admin_org_notification_email_body_part1" t={language} values={{ orgSlug }}>
An organization with slug {`"${orgSlug}"`} was created.
<br />
<br />
Please be sure to configure your DNS registry to point the subdomain corresponding to the new
organization to where the main app is running. Otherwise the organization will not work.
<br />
<br />
Here are just the very basic options to configure a subdomain to point to their app so it loads the
organization profile page.
<br />
<br />
You can do it either with the A Record:
</Trans>
</p>
{dnsTable("A", orgSlug, webappIPAddress, language)}
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
{language("admin_org_notification_email_body_part2")}
</p>
{dnsTable("CNAME", orgSlug, webAppUrl, language)}
<p style={{ fontWeight: 400, lineHeight: "24px" }}>
{language("admin_org_notification_email_body_part3")}
</p>
</BaseEmailHtml>
);
};
+1
View File
@@ -29,3 +29,4 @@ export * from "@calcom/app-store/routing-forms/emails/components";
export { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail";
export { AdminOrganizationNotificationEmail } from "./AdminOrganizationNotificationEmail";
@@ -0,0 +1,43 @@
import type { TFunction } from "next-i18next";
import { APP_NAME } from "@calcom/lib/constants";
import { renderEmail } from "../";
import BaseEmail from "./_base-email";
export type OrganizationNotification = {
t: TFunction;
instanceAdmins: { email: string }[];
ownerEmail: string;
orgSlug: string;
webappIPAddress: string;
};
export default class AdminOrganizationNotification extends BaseEmail {
input: OrganizationNotification;
constructor(input: OrganizationNotification) {
super();
this.name = "SEND_ADMIN_ORG_NOTIFICATION";
this.input = input;
}
protected getNodeMailerPayload(): Record<string, unknown> {
return {
from: `${APP_NAME} <${this.getMailerOptions().from}>`,
to: this.input.instanceAdmins.map((admin) => admin.email).join(","),
subject: `${this.input.t("admin_org_notification_email_subject")}`,
html: renderEmail("AdminOrganizationNotificationEmail", {
orgSlug: this.input.orgSlug,
webappIPAddress: this.input.webappIPAddress,
language: this.input.t,
}),
text: this.getTextBody(),
};
}
protected getTextBody(): string {
return `${this.input.t("hi_admin")}, ${this.input.t("admin_org_notification_email_title").toLowerCase()}
${this.input.t("admin_org_notification_email_body")}`.trim();
}
}
@@ -1,6 +1,7 @@
import type { Page, Frame } from "@playwright/test";
import { test, expect } from "@playwright/test";
// eslint-disable-next-line no-restricted-imports
import prisma from "@calcom/prisma";
export function todo(title: string) {
-1
View File
@@ -562,7 +562,6 @@ class CalApi {
modal({
calLink,
calOrigin,
config = {},
uid,
}: {
+40 -4
View File
@@ -1,11 +1,13 @@
import { LazyMotion, domAnimation, m, AnimatePresence } from "framer-motion";
import { LazyMotion, m, AnimatePresence } from "framer-motion";
import dynamic from "next/dynamic";
import { useEffect, useRef } from "react";
import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import dayjs from "@calcom/dayjs";
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
@@ -26,6 +28,7 @@ import { validateLayout } from "./utils/layout";
import { getQueryParam } from "./utils/query-param";
import { useBrandColors } from "./utils/use-brand-colors";
const loadFramerFeatures = () => import("./framer-features").then((res) => res.default);
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity));
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), {
@@ -73,6 +76,9 @@ const BookerComponent = ({
// In Embed we give preference to embed configuration for the layout.If that's not set, we use the App configuration for the event layout
// But if it's mobile view, there is only one layout supported which is 'mobile'
const layout = isEmbed ? (isMobile ? "mobile" : validateLayout(embedUiConfig.layout) || _layout) : _layout;
const columnViewExtraDays = useRef<number>(
isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop
);
const [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -86,9 +92,36 @@ const BookerComponent = ({
shallow
);
const date = dayjs(selectedDate).format("YYYY-MM-DD");
const schedule = useScheduleForEvent({ prefetchNextMonth: true });
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots).filter(
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
);
const extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
const animationScope = useBookerResizeAnimation(layout, bookerState);
const totalWeekDays = 7;
const addonDays =
nonEmptyScheduleDays.length < extraDays
? (extraDays - nonEmptyScheduleDays.length + 1) * totalWeekDays
: nonEmptyScheduleDays.length === extraDays
? totalWeekDays
: 0;
//Taking one more avaliable slot(extraDays + 1) to claculate the no of days in between, that next and prev button need to shift.
const availableSlots = nonEmptyScheduleDays.slice(0, extraDays + 1);
if (nonEmptyScheduleDays.length !== 0)
columnViewExtraDays.current =
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 2], "day")) + addonDays;
const prefetchNextMonth =
dayjs(date).month() !== dayjs(date).add(columnViewExtraDays.current, "day").month();
const monthCount =
dayjs(date).add(1, "month").month() !== dayjs(date).add(columnViewExtraDays.current, "day").month()
? 2
: undefined;
const nextSlots =
Math.abs(dayjs(selectedDate).diff(availableSlots[availableSlots.length - 1], "day")) + addonDays;
// I would expect isEmbed to be not needed here as it's handled in derived variable layout, but somehow removing it breaks the views.
const defaultLayout = isEmbed
@@ -213,8 +246,9 @@ const BookerComponent = ({
)}>
<Header
enabledLayouts={bookerLayouts.enabledLayouts}
extraDays={extraDays}
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
isMobile={isMobile}
nextSlots={nextSlots}
/>
</BookerSection>
<StickyOnDesktop
@@ -280,7 +314,7 @@ const BookerComponent = ({
layout === BookerLayouts.COLUMN_VIEW
}
className={classNames(
"border-subtle rtl:border-default flex h-full w-full flex-col px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
"border-subtle rtl:border-default flex h-full w-full flex-col overflow-x-auto px-5 py-3 pb-0 rtl:border-r ltr:md:border-l",
layout === BookerLayouts.MONTH_VIEW &&
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
@@ -290,6 +324,8 @@ const BookerComponent = ({
<AvailableTimeSlots
extraDays={extraDays}
limitHeight={layout === BookerLayouts.MONTH_VIEW}
prefetchNextMonth={prefetchNextMonth}
monthCount={monthCount}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
/>
</BookerSection>
@@ -319,7 +355,7 @@ export const Booker = (props: BookerProps) => {
if (props.isAway) return <Away />;
return (
<LazyMotion features={domAnimation}>
<LazyMotion strict features={loadFramerFeatures}>
<BookerComponent {...props} />
</LazyMotion>
);
@@ -1,11 +1,14 @@
import { useMemo, useRef, useEffect } from "react";
import { useRef, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings";
import { useSlotsForMultipleDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import { useSlotsForAvailableDates } from "@calcom/features/schedules/lib/use-schedule/useSlotsForDate";
import { classNames } from "@calcom/lib";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc";
import { useBookerStore } from "../store";
import { useEvent, useScheduleForEvent } from "../utils/event";
@@ -13,6 +16,8 @@ import { useEvent, useScheduleForEvent } from "../utils/event";
type AvailableTimeSlotsProps = {
extraDays?: number;
limitHeight?: boolean;
prefetchNextMonth: boolean;
monthCount: number | undefined;
seatsPerTimeSlot?: number | null;
};
@@ -23,7 +28,14 @@ type AvailableTimeSlotsProps = {
* will also fetch the next `extraDays` days and show multiple days
* in columns next to each other.
*/
export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }: AvailableTimeSlotsProps) => {
export const AvailableTimeSlots = ({
extraDays,
limitHeight,
seatsPerTimeSlot,
prefetchNextMonth,
monthCount,
}: AvailableTimeSlotsProps) => {
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
const isMobile = useMediaQuery("(max-width: 768px)");
const selectedDate = useBookerStore((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
@@ -31,6 +43,8 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
const isEmbed = useIsEmbed();
const event = useEvent();
const date = selectedDate || dayjs().format("YYYY-MM-DD");
const [layout] = useBookerStore((state) => [state.layout]);
const isColumnView = layout === BookerLayouts.COLUMN_VIEW;
const containerRef = useRef<HTMLDivElement | null>(null);
const onTimeSelect = (
@@ -57,29 +71,23 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
};
const schedule = useScheduleForEvent({
prefetchNextMonth: !!extraDays && dayjs(date).month() !== dayjs(date).add(extraDays, "day").month(),
prefetchNextMonth,
monthCount,
});
const nonEmptyScheduleDays = useNonEmptyScheduleDays(schedule?.data?.slots);
const nonEmptyScheduleDaysFromSelectedDate = nonEmptyScheduleDays.filter(
(slot) => dayjs(selectedDate).diff(slot, "day") <= 0
);
// Creates an array of dates to fetch slots for.
// If `extraDays` is passed in, we will extend the array with the next `extraDays` days.
const dates = useMemo(
() =>
!extraDays
? [date]
: [
// If NO date is selected yet, we show by default the upcomming `nextDays` days.
date,
...Array.from({ length: extraDays }).map((_, index) =>
dayjs(date)
.add(index + 1, "day")
.format("YYYY-MM-DD")
),
],
[date, extraDays]
);
const dates = !extraDays
? [date]
: nonEmptyScheduleDaysFromSelectedDate.length > 0
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays)
: [];
const isMultipleDates = dates.length > 1;
const slotsPerDay = useSlotsForMultipleDates(dates, schedule?.data?.slots);
const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
useEffect(() => {
if (isEmbed) return;
@@ -103,11 +111,16 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
<AvailableTimes
className="w-full"
key={slots.date}
showTimeFormatToggle={!isColumnView}
onTimeSelect={onTimeSelect}
date={dayjs(slots.date)}
slots={slots.slots}
onTimeSelect={onTimeSelect}
seatsPerTimeSlot={seatsPerTimeSlot}
showTimeFormatToggle={!isMultipleDates}
availableMonth={
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
? dayjs(slots.date).format("MMM")
: undefined
}
/>
))}
</div>
@@ -63,7 +63,12 @@ export const EventMeta = () => {
)}
{!isLoading && !!event && (
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
<EventMembers schedulingType={event.schedulingType} users={event.users} profile={event.profile} />
<EventMembers
schedulingType={event.schedulingType}
users={event.users}
profile={event.profile}
entity={event.entity}
/>
<EventTitle className="my-2">{event?.title}</EventTitle>
{event.description && (
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
@@ -16,10 +16,12 @@ export function Header({
extraDays,
isMobile,
enabledLayouts,
nextSlots,
}: {
extraDays: number;
isMobile: boolean;
enabledLayouts: BookerLayouts[];
nextSlots: number;
}) {
const { t, i18n } = useLocale();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
@@ -56,8 +58,7 @@ export function Header({
if (isMonthView) {
return <LayoutToggleWithData />;
}
const endDate = selectedDate.add(extraDays - 1, "days");
const endDate = selectedDate.add(layout === BookerLayouts.COLUMN_VIEW ? extraDays : extraDays - 1, "days");
const isSameMonth = () => {
return selectedDate.format("MMM") === endDate.format("MMM");
@@ -91,7 +92,7 @@ export function Header({
color="minimal"
StartIcon={ChevronLeft}
aria-label="Previous Day"
onClick={() => addToSelectedDate(-extraDays)}
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)}
/>
<Button
className="group rtl:mr-1 rtl:rotate-180"
@@ -99,7 +100,7 @@ export function Header({
color="minimal"
StartIcon={ChevronRight}
aria-label="Next Day"
onClick={() => addToSelectedDate(extraDays)}
onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)}
/>
{selectedDateMin3DaysDifference && (
<Button
+1 -1
View File
@@ -224,7 +224,7 @@ export const extraDaysConfig = {
tablet: 4,
},
[BookerLayouts.COLUMN_VIEW]: {
desktop: 4,
desktop: 6,
tablet: 2,
},
};
@@ -0,0 +1,3 @@
import { domAnimation } from "framer-motion";
export default domAnimation;
@@ -45,6 +45,7 @@ export const useScheduleForEvent = ({
eventId,
month,
duration,
monthCount,
}: {
prefetchNextMonth?: boolean;
username?: string | null;
@@ -52,6 +53,7 @@ export const useScheduleForEvent = ({
eventId?: number | null;
month?: string | null;
duration?: number | null;
monthCount?: number;
} = {}) => {
const { timezone } = useTimePreferences();
const event = useEvent();
@@ -70,6 +72,7 @@ export const useScheduleForEvent = ({
eventId: event.data?.id ?? eventId,
timezone,
prefetchNextMonth,
monthCount,
rescheduleUid,
month: monthFromStore ?? month,
duration: durationFromStore ?? duration,
@@ -26,6 +26,7 @@ type AvailableTimesProps = {
seatsPerTimeSlot?: number | null;
showTimeFormatToggle?: boolean;
className?: string;
availableMonth?: string | undefined;
selectedSlots?: string[];
};
@@ -36,6 +37,7 @@ export const AvailableTimes = ({
seatsPerTimeSlot,
showTimeFormatToggle = true,
className,
availableMonth,
selectedSlots,
}: AvailableTimesProps) => {
const { t, i18n } = useLocale();
@@ -65,6 +67,7 @@ export const AvailableTimes = ({
isMonthView ? "text-default text-sm" : "text-xs"
)}>
{date.format("DD")}
{availableMonth && `, ${availableMonth}`}
</span>
</span>
@@ -1,4 +1,5 @@
import { CAL_URL } from "@calcom/lib/constants";
import { getOrgFullDomain } from "@calcom/features/ee/organizations/lib/orgDomains";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { SchedulingType } from "@calcom/prisma/enums";
import { AvatarGroup } from "@calcom/ui";
@@ -12,6 +13,7 @@ export interface EventMembersProps {
schedulingType: PublicEvent["schedulingType"];
users: PublicEvent["users"];
profile: PublicEvent["profile"];
entity: PublicEvent["entity"];
}
type Avatar = {
@@ -23,7 +25,7 @@ type Avatar = {
type AvatarWithRequiredImage = Avatar & { image: string };
export const EventMembers = ({ schedulingType, users, profile }: EventMembersProps) => {
export const EventMembers = ({ schedulingType, users, profile, entity }: EventMembersProps) => {
const showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
const shownUsers = showMembers ? users : [];
@@ -37,9 +39,19 @@ export const EventMembers = ({ schedulingType, users, profile }: EventMembersPro
title: `${user.name}`,
image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`,
alt: user.name || undefined,
href: user.username ? `${CAL_URL}/${user.username}` : undefined,
href: `/${user.username}`,
}));
// Add organization avatar
if (entity.orgSlug) {
avatars.unshift({
title: `${entity.name}`,
image: `${WEBAPP_URL}/team/${entity.orgSlug}/avatar.png`,
alt: entity.name || undefined,
href: getOrgFullDomain(entity.orgSlug),
});
}
// Add profile later since we don't want to force creating an avatar for this if it doesn't exist.
avatars.unshift({
title: `${profile.name}`,
@@ -1,8 +1,9 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { z } from "zod";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify";
import type { PrismaClient } from "@calcom/prisma";
import prisma from "@calcom/prisma";
type BookingSelect = {
@@ -223,7 +223,7 @@ function preprocess<T extends z.ZodType>({
if (optionValue) {
// `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue`
if (typeOfOptionInput === "phone") {
if (!phoneSchema.safeParse(optionValue).success) {
if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") });
}
}
@@ -1,4 +1,4 @@
import type { Prisma, PrismaClient, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import type { EventManagerUser } from "@calcom/core/EventManager";
@@ -11,6 +11,7 @@ import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@@ -107,7 +107,7 @@ export default function ApiKeyDialogForm({
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
</span>
</div>
<DialogFooter showDivider className="!mt-12">
<DialogFooter showDivider className="relative">
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
{t("done")}
</Button>
@@ -209,7 +209,7 @@ export default function ApiKeyDialogForm({
</div>
)}
<DialogFooter showDivider className="!mt-12">
<DialogFooter showDivider className="relative">
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
{t("cancel")}
</Button>
@@ -1,8 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import cache from "memory-cache";
import { z } from "zod";
import { CONSOLE_URL } from "@calcom/lib/constants";
import type { PrismaClient } from "@calcom/prisma";
const CACHING_TIME = 86400000; // 24 hours in milliseconds
@@ -1,4 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
export async function getDeploymentKey(prisma: PrismaClient) {
const deployment = await prisma.deployment.findUnique({
@@ -1,9 +1,10 @@
import type { PrismaClient, Prisma } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums";
import { _EventTypeModel } from "@calcom/prisma/zod";
import { allManagedEventTypeProps, unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
@@ -48,7 +48,7 @@ export function subdomainSuffix() {
}
export function getOrgFullDomain(slug: string, options: { protocol: boolean } = { protocol: true }) {
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}/`;
return `${options.protocol ? `${new URL(WEBAPP_URL).protocol}//` : ""}${slug}.${subdomainSuffix()}`;
}
export function getSlugOrRequestedSlug(slug: string) {
@@ -0,0 +1,145 @@
import NoSSR from "@calcom/core/components/NoSSR";
import LicenseRequired from "@calcom/ee/common/components/LicenseRequired";
import { extractDomainFromWebsiteUrl } from "@calcom/ee/organizations/lib/utils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Meta, DropdownActions, showToast, Table, Badge } from "@calcom/ui";
import { X, Check, CheckCheck } from "@calcom/ui/components/icon";
import { getLayout } from "../../../../../settings/layouts/SettingsLayout";
const { Body, Cell, ColumnTitle, Header, Row } = Table;
function AdminOrgTable() {
const { t } = useLocale();
const utils = trpc.useContext();
const [data] = trpc.viewer.organizations.adminGetAll.useSuspenseQuery();
const verifyMutation = trpc.viewer.organizations.adminVerify.useMutation({
onSuccess: async () => {
showToast(t("org_has_been_processed"), "success");
await utils.viewer.organizations.adminGetAll.invalidate();
},
onError: (err) => {
console.error(err.message);
showToast(t("org_error_processing"), "error");
},
});
const updateMutation = trpc.viewer.organizations.update.useMutation({
onSuccess: async () => {
showToast(t("org_has_been_processed"), "success");
await utils.viewer.organizations.adminGetAll.invalidate();
},
onError: (err) => {
console.error(err.message);
showToast(t("org_error_processing"), "error");
},
});
return (
<div>
<Table>
<Header>
<ColumnTitle widthClassNames="w-auto">{t("organization")}</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">{t("owner")}</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">
<span className="sr-only">{t("edit")}</span>
</ColumnTitle>
</Header>
<Body>
{data.map((org) => (
<Row key={org.id}>
<Cell widthClassNames="w-auto">
<div className="text-subtle font-medium">
<span className="text-default">{org.name}</span>
<br />
<span className="text-muted">
{org.slug}.{extractDomainFromWebsiteUrl}
</span>
</div>
</Cell>
<Cell widthClassNames="w-auto">
<span className="break-all">{org.members[0].user.email}</span>
</Cell>
<Cell>
<div className="space-x-2">
{!org.metadata?.isOrganizationVerified && <Badge variant="blue">{t("unverified")}</Badge>}
{!org.metadata?.isOrganizationConfigured && <Badge variant="red">{t("dns_missing")}</Badge>}
</div>
</Cell>
<Cell widthClassNames="w-auto">
<div className="flex w-full justify-end">
{(!org.metadata?.isOrganizationVerified || !org.metadata?.isOrganizationConfigured) && (
<DropdownActions
actions={[
...(!org.metadata?.isOrganizationVerified
? [
{
id: "accept",
label: t("accept"),
onClick: () => {
verifyMutation.mutate({
orgId: org.id,
status: "ACCEPT",
});
},
icon: Check,
},
{
id: "reject",
label: t("reject"),
onClick: () => {
verifyMutation.mutate({
orgId: org.id,
status: "DENY",
});
},
icon: X,
},
]
: []),
...(!org.metadata?.isOrganizationConfigured
? [
{
id: "dns",
label: t("mark_dns_configured"),
onClick: () => {
updateMutation.mutate({
orgId: org.id,
metadata: {
isOrganizationConfigured: true,
},
});
},
icon: CheckCheck,
},
]
: []),
]}
/>
)}
</div>
</Cell>
</Row>
))}
</Body>
</Table>
</div>
);
}
const AdminOrgList = () => {
const { t } = useLocale();
return (
<LicenseRequired>
<Meta title={t("organizations")} description={t("orgs_page_description")} />
<NoSSR>
<AdminOrgTable />
</NoSSR>
</LicenseRequired>
);
};
AdminOrgList.getLayout = getLayout;
export default AdminOrgList;
@@ -1,108 +0,0 @@
import NoSSR from "@calcom/core/components/NoSSR";
import LicenseRequired from "@calcom/ee/common/components/LicenseRequired";
import { extractDomainFromWebsiteUrl } from "@calcom/ee/organizations/lib/utils";
import { trpc } from "@calcom/trpc/react";
import { Meta } from "@calcom/ui";
import { DropdownActions, showToast, Table } from "@calcom/ui";
import { Check, X } from "@calcom/ui/components/icon";
import { getLayout } from "../../../../../settings/layouts/SettingsLayout";
const { Body, Cell, ColumnTitle, Header, Row } = Table;
function UnverifiedOrgTable() {
const utils = trpc.useContext();
const [data] = trpc.viewer.organizations.adminGetUnverified.useSuspenseQuery();
const mutation = trpc.viewer.organizations.adminVerify.useMutation({
onSuccess: async () => {
showToast("Org has been processed", "success");
await utils.viewer.organizations.adminGetUnverified.invalidate();
},
onError: (err) => {
console.error(err.message);
showToast("There has been an error processing this org.", "error");
},
});
return (
<div>
<Table>
<Header>
<ColumnTitle widthClassNames="w-auto">Organization</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">Owner</ColumnTitle>
<ColumnTitle widthClassNames="w-auto">
<span className="sr-only">Edit</span>
</ColumnTitle>
</Header>
<Body>
{data.map((org) => (
<Row key={org.id}>
<Cell widthClassNames="w-auto">
<div className="text-subtle font-medium">
<span className="text-default">{org.name}</span>
<br />
<span className="text-muted">
{org.slug}.{extractDomainFromWebsiteUrl}
</span>
</div>
</Cell>
<Cell widthClassNames="w-auto">
<span className="break-all">{org.members[0].user.email}</span>
</Cell>
<Cell widthClassNames="w-auto">
<div className="flex w-full justify-end">
<DropdownActions
actions={[
{
id: "accept",
label: "Accept",
onClick: () => {
mutation.mutate({
orgId: org.id,
status: "ACCEPT",
});
},
icon: Check,
},
{
id: "reject",
label: "Reject",
onClick: () => {
mutation.mutate({
orgId: org.id,
status: "DENY",
});
},
icon: X,
},
]}
/>
</div>
</Cell>
</Row>
))}
</Body>
</Table>
</div>
);
}
const UnverifiedOrgList = () => {
return (
<LicenseRequired>
<Meta
title="Organizations"
description="A list of all organizations that need verification based on their email domain. Accepting an organization will allow all users with that email domain to sign up WITHOUT email verifciation."
/>
<NoSSR>
<UnverifiedOrgTable />
</NoSSR>
</LicenseRequired>
);
};
UnverifiedOrgList.getLayout = getLayout;
export default UnverifiedOrgList;
@@ -57,7 +57,7 @@ const OrgProfileView = () => {
},
async onSuccess() {
await utils.viewer.teams.get.invalidate();
showToast(t("your_organisation_updated_sucessfully"), "success");
showToast(t("your_organization_updated_sucessfully"), "success");
},
});
+1 -1
View File
@@ -1,8 +1,8 @@
import type { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
import type { PrismaClient } from "@prisma/client";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
@@ -35,6 +35,7 @@ export default function TeamInviteList(props: Props) {
const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
async onSuccess() {
await utils.viewer.teams.list.invalidate();
await utils.viewer.teams.get.invalidate();
await utils.viewer.organizations.listMembers.invalidate();
},
async onError(err) {
@@ -11,6 +11,7 @@ import {
DropdownMenuItem,
DropdownItem,
DropdownMenuTrigger,
showToast,
} from "@calcom/ui";
import { Ban, Check, MoreHorizontal, X } from "@calcom/ui/components/icon";
@@ -39,7 +40,9 @@ export default function TeamInviteListItem(props: Props) {
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: async () => {
showToast(t("success"), "success");
await utils.viewer.teams.get.invalidate();
await utils.viewer.teams.hasTeamPlan.invalidate();
await utils.viewer.teams.list.invalidate();
await utils.viewer.organizations.listMembers.invalidate();
},
@@ -67,7 +67,10 @@ export default function TeamListItem(props: Props) {
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: () => {
showToast(t("success"), "success");
utils.viewer.teams.get.invalidate();
utils.viewer.teams.list.invalidate();
utils.viewer.teams.hasTeamPlan.invalidate();
utils.viewer.teams.listInvites.invalidate();
},
});
@@ -9,7 +9,7 @@ import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/or
import { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { PrismaClient } from "@calcom/prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
import {
bookerLayoutOptions,
+4 -2
View File
@@ -1,10 +1,12 @@
import type { AppFlags } from "@calcom/features/flags/config";
import { trpc } from "@calcom/trpc/react";
export function useFlags() {
const query = trpc.viewer.features.map.useQuery(undefined, {
initialData: process.env.NEXT_PUBLIC_IS_E2E
? { "managed-event-types": true, organizations: true, teams: true }
: {},
: undefined,
placeholderData: {},
});
return query.data;
return query.data ?? ({} as AppFlags);
}
+1 -1
View File
@@ -1,4 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
import type { AppFlags } from "../config";
@@ -10,6 +10,7 @@ type UseScheduleWithCacheArgs = {
timezone?: string | null;
prefetchNextMonth?: boolean;
duration?: number | null;
monthCount?: number | null;
rescheduleUid?: string | null;
isTeamEvent?: boolean;
};
@@ -22,11 +23,12 @@ export const useSchedule = ({
eventId,
prefetchNextMonth,
duration,
monthCount,
rescheduleUid,
isTeamEvent,
}: UseScheduleWithCacheArgs) => {
const monthDayjs = month ? dayjs(month) : dayjs();
const nextMonthDayjs = monthDayjs.add(1, "month");
const nextMonthDayjs = monthDayjs.add(monthCount ? monthCount : 1, "month");
// Why the non-null assertions? All of these arguments are checked in the enabled condition,
// and the query will not run if they are null. However, the check in `enabled` does
// no satisfy typscript.
@@ -7,6 +7,7 @@ import type { Slots } from "./types";
* @param date Format YYYY-MM-DD
* @param scheduleCache Instance of useScheduleWithCache
*/
export const useSlotsForDate = (date: string | null, slots?: Slots) => {
const slotsForDate = useMemo(() => {
if (!date || typeof slots === "undefined") return [];
@@ -16,9 +17,9 @@ export const useSlotsForDate = (date: string | null, slots?: Slots) => {
return slotsForDate;
};
export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots) => {
export const useSlotsForAvailableDates = (dates: (string | null)[], slots?: Slots) => {
const slotsForDates = useMemo(() => {
if (typeof slots === "undefined") return [];
if (slots === undefined) return [];
return dates
.filter((date) => date !== null)
.map((date) => ({
@@ -26,6 +27,5 @@ export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots
date,
}));
}, [dates, slots]);
return slotsForDates;
};
@@ -5,6 +5,7 @@ import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
export default function TimezoneChangeDialog() {
const { t } = useLocale();
@@ -43,7 +44,7 @@ export default function TimezoneChangeDialog() {
const tzDifferent =
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
setOpen(showDialog);
setOpen(!IS_VISUAL_REGRESSION_TESTING && showDialog);
}, [currentTz, isLoading, userTz]);
// save cookie to not show again
+2 -1
View File
@@ -77,6 +77,7 @@ import {
Zap,
} from "@calcom/ui/components/icon";
import { Discord } from "@calcom/ui/components/icon/Discord";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import { useOrgBranding } from "../ee/organizations/context/provider";
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
@@ -920,7 +921,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
</ButtonOrLink>
</Tooltip>
))}
<Credits />
{!IS_VISUAL_REGRESSION_TESTING && <Credits />}
</div>
</aside>
</div>
@@ -3,11 +3,14 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useMemo, useRef, useCallback, useEffect, useState } from "react";
import dayjs from "@calcom/dayjs";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import type { DateRange } from "@calcom/lib/date-ranges";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { TBContext, createTimezoneBuddyStore } from "../store";
import { TimeDial } from "./TimeDial";
@@ -20,6 +23,32 @@ export interface SliderUser {
dateRanges: DateRange[];
}
function UpgradeTeamTip() {
const { t } = useLocale();
return (
<UpgradeTip
title={t("calcom_is_better_with_team", { appName: APP_NAME }) as string}
description="add_your_team_members"
background="/tips/teams"
features={[]}
buttons={
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
<ButtonGroup>
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
{t("create_team")}
</Button>
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
{t("learn_more")}
</Button>
</ButtonGroup>
</div>
}>
<></>
</UpgradeTip>
);
}
export function AvailabilitySliderTable() {
const tableContainerRef = useRef<HTMLDivElement>(null);
const [browsingDate, setBrowsingDate] = useState(dayjs());
@@ -143,6 +172,9 @@ export function AvailabilitySliderTable() {
fetchMoreOnBottomReached(tableContainerRef.current);
}, [fetchMoreOnBottomReached]);
// This means they are not apart of any teams so we show the upgrade tip
if (!flatData.length) return <UpgradeTeamTip />;
return (
<TBContext.Provider
value={createTimezoneBuddyStore({
@@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import defaultPrisma from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
export type GetSubscriberOptions = {
+2 -1
View File
@@ -1,8 +1,9 @@
import type { Prisma, PrismaClient } from "@prisma/client";
import type { Prisma } from "@prisma/client";
import type { z } from "zod";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify";
import type { PrismaClient } from "@calcom/prisma";
type BookingSelect = {
description: true;
+1 -1
View File
@@ -1,4 +1,3 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client";
import { getLocationGroupedOptions } from "@calcom/app-store/server";
@@ -10,6 +9,7 @@ import { parseBookingLimit, parseDurationLimit, parseRecurringEvent } from "@cal
import { CAL_URL } from "@calcom/lib/constants";
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
+32
View File
@@ -0,0 +1,32 @@
import type { SomeZodObject, z } from "zod";
import objectKeys from "./objectKeys";
/**
* The intention behind these helpers is to make it easier to work with metadata.
* @param schema This is the zod schema that you want to use to parse the metadata
* @param rawMetadata This is the metadata that you want to parse
* @returns An object with the parsed metadata, a get function to get a specific key of the metadata, and a mergeMetadata function to merge new metadata with the old one
* @example
* const { mergeMetadata } = getMetadataHelpers(teamMetadataSchema, team.metadata);
* const newMetadata = mergeMetadata({ someKey: "someValue" });
* prisma.team.update({ ..., data: { metadata: newMetadata } });
*/
export function getMetadataHelpers<T extends SomeZodObject>(schema: T, rawMetadata: unknown) {
const metadata = schema.parse(rawMetadata) as z.infer<T>;
return {
metadata,
get: (key: keyof z.infer<T>) => metadata[key],
/** This method prevents overwriting the metadata fields that you don't want to change. */
mergeMetadata: (newMetadata: z.infer<T>) => {
const newMetadataToReturn = { ...metadata, ...newMetadata };
// We check for each key of newMetadata and if it's explicitly undefined, we delete it from newMetadataToReturn
objectKeys(newMetadata).forEach((key) => {
if (newMetadata[key] === undefined) {
delete newMetadataToReturn[key];
}
});
return newMetadataToReturn as z.infer<T>;
},
};
}
+1
View File
@@ -57,6 +57,7 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
search.set(String(key), String(value));
router.replace(`${pathname}?${search.toString()}`);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[parsedQuery, router]
);
@@ -1,4 +1,4 @@
import type { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@calcom/prisma";
export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: string) {
// Look bookingUid in bookingSeat
+3 -1
View File
@@ -8,10 +8,12 @@ describe("slugify", () => {
expect(slugify("HELLO")).toEqual("hello");
});
it("should convert spaces, _, and any other special character to -", () => {
it("should convert spaces, _, +, # and any other special character to -", () => {
expect(slugify("hello there")).toEqual("hello-there");
expect(slugify("hello_there")).toEqual("hello-there");
expect(slugify("hello$there")).toEqual("hello-there");
expect(slugify("hello+there")).toEqual("hello-there");
expect(slugify("#hellothere")).toEqual("hellothere");
});
it("should keep numbers as is", () => {
+2 -3
View File
@@ -5,10 +5,9 @@ export const slugify = (str: string) => {
.normalize("NFD") // Normalize to decomposed form for handling accents
.replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters
.replace(/[^\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode) with a dash
.replace(/[\s_]+/g, "-") // Replace whitespace and underscores with a single dash
.replace(/[\s_#]+/g, "-") // Replace whitespace, # and underscores with a single dash
.replace(/^-+/, "") // Remove dashes from start
.replace(/-+$/, "") // Remove dashes from end
.replace(/\+/g, "-"); // Transform all + to -
.replace(/-+$/, ""); // Remove dashes from end
};
export default slugify;
+7
View File
@@ -31,6 +31,13 @@ turndownService.addRule("enter", {
},
});
turndownService.addRule("ignoreEmphasized", {
filter: "em",
replacement: function (content) {
return content;
},
});
function isShiftEnter(node: HTMLElement) {
let currentNode: HTMLElement | null | ParentNode = node;
+43 -6
View File
@@ -1,28 +1,65 @@
import type { Prisma } from "@prisma/client";
import { PrismaClient } from "@prisma/client";
import { PrismaClient as PrismaClientWithoutExtension } from "@prisma/client";
import { bookingReferenceMiddleware } from "./middleware";
declare global {
// eslint-disable-next-line no-var
var prisma: PrismaClient | undefined;
var prisma: typeof prismaWithClientExtensions;
}
const prismaOptions: Prisma.PrismaClientOptions = {};
if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"];
export const prisma = globalThis.prisma || new PrismaClient(prismaOptions);
const prismaWithoutClientExtensions = new PrismaClientWithoutExtension(prismaOptions);
export const customPrisma = (options: Prisma.PrismaClientOptions) =>
new PrismaClient({ ...prismaOptions, ...options });
new PrismaClientWithoutExtension({ ...prismaOptions, ...options });
// If any changed on middleware server restart is required
// TODO: Migrate it to $extends
bookingReferenceMiddleware(prismaWithoutClientExtensions);
// FIXME: Due to some reason, there are types failing in certain places due to the $extends. Fix it and then enable it
// Specifically we get errors like `Type 'string | Date | null | undefined' is not assignable to type 'Exact<string | Date | null | undefined, string | Date | null | undefined>'`
// const prismaWithClientExtensions = prismaWithoutClientExtensions.$extends({
// query: {
// $allModels: {
// async $allOperations({ model, operation, args, query }) {
// const start = performance.now();
// /* your custom logic here */
// const res = await query(args);
// const end = performance.now();
// logger.debug("Query Perf: ", `${model}.${operation} took ${(end - start).toFixed(2)}ms\n`);
// return res;
// },
// },
// },
// });
// .$extends({
// name: "teamUpdateWithMetadata",
// query: {
// team: {
// async update({ model, operation, args, query }) {
// if (args.data.metadata) {
// // Prepare args.data with merged metadata
// }
// return query(args);
// },
// },
// },
// })
const prismaWithClientExtensions = prismaWithoutClientExtensions;
export const prisma = (globalThis.prisma as typeof prismaWithClientExtensions) || prismaWithClientExtensions;
if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma;
}
// If any changed on middleware server restart is required
bookingReferenceMiddleware(prisma);
export type PrismaClient = typeof prisma;
export default prisma;
export * from "./selects";
@@ -1,4 +1,4 @@
import { PrismaClient } from "@prisma/client";
import type { PrismaClient } from "@prisma/client";
function middleware(prisma: PrismaClient) {
/***********************************/
+2
View File
@@ -79,7 +79,9 @@ model EventType {
bookingFields Json?
timeZone String?
periodType PeriodType @default(UNLIMITED)
/// @zod.custom(imports.coerceToDate)
periodStartDate DateTime?
/// @zod.custom(imports.coerceToDate)
periodEndDate DateTime?
periodDays Int?
periodCountCalendarDays Boolean?

Some files were not shown because too many files have changed in this diff Show More