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 2902 additions and 795 deletions

No files matched your search

+7 -1
View File
@@ -33,12 +33,14 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
position: true, position: true,
eventName: true, eventName: true,
timeZone: true, timeZone: true,
schedulingType: true,
// START Limit future bookings
periodType: true, periodType: true,
periodStartDate: true, periodStartDate: true,
schedulingType: true,
periodEndDate: true, periodEndDate: true,
periodDays: true, periodDays: true,
periodCountCalendarDays: true, periodCountCalendarDays: true,
// END Limit future bookings
requiresConfirmation: true, requiresConfirmation: true,
disableGuests: true, disableGuests: true,
hideCalendarNotes: true, hideCalendarNotes: true,
@@ -51,6 +53,8 @@ export const schemaEventTypeBaseBodyParams = EventType.pick({
slotInterval: true, slotInterval: true,
successRedirectUrl: true, successRedirectUrl: true,
locations: true, locations: true,
bookingLimits: true,
durationLimits: true,
}) })
.merge(z.object({ hosts: z.array(hostSchema).optional().default([]) })) .merge(z.object({ hosts: z.array(hostSchema).optional().default([]) }))
.partial() .partial()
@@ -126,6 +130,8 @@ export const schemaEventTypeReadPublic = EventType.pick({
seatsPerTimeSlot: true, seatsPerTimeSlot: true,
seatsShowAttendees: true, seatsShowAttendees: true,
bookingFields: true, bookingFields: true,
bookingLimits: true,
durationLimits: true,
}).merge( }).merge(
z.object({ z.object({
locations: z 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 { NextApiRequest } from "next";
import type { z } from "zod"; import type { z } from "zod";
@@ -202,10 +202,17 @@ import checkTeamEventEditPermission from "../_utils/checkTeamEventEditPermission
export async function patchHandler(req: NextApiRequest) { export async function patchHandler(req: NextApiRequest) {
const { prisma, query, body } = req; const { prisma, query, body } = req;
const { id } = schemaQueryIdParseInt.parse(query); const { id } = schemaQueryIdParseInt.parse(query);
const { hosts = [], ...parsedBody } = schemaEventTypeEditBodyParams.parse(body); const {
hosts = [],
bookingLimits,
durationLimits,
...parsedBody
} = schemaEventTypeEditBodyParams.parse(body);
const data: Prisma.EventTypeUpdateArgs["data"] = { const data: Prisma.EventTypeUpdateArgs["data"] = {
...parsedBody, ...parsedBody,
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
}; };
if (hosts) { 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 type { NextApiRequest } from "next";
import { HttpError } from "@calcom/lib/http-error"; import { HttpError } from "@calcom/lib/http-error";
@@ -259,12 +259,19 @@ import ensureOnlyMembersAsHosts from "./_utils/ensureOnlyMembersAsHosts";
async function postHandler(req: NextApiRequest) { async function postHandler(req: NextApiRequest) {
const { userId, isAdmin, prisma, body } = req; 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"] = { let data: Prisma.EventTypeCreateArgs["data"] = {
...parsedBody, ...parsedBody,
userId, userId,
users: { connect: { id: userId } }, users: { connect: { id: userId } },
bookingLimits: bookingLimits === null ? Prisma.DbNull : bookingLimits,
durationLimits: durationLimits === null ? Prisma.DbNull : durationLimits,
}; };
await checkPermissions(req); await checkPermissions(req);
+20
View File
@@ -4,6 +4,7 @@ import type { NextApiRequest } from "next";
import { defaultResponder } from "@calcom/lib/server"; import { defaultResponder } from "@calcom/lib/server";
import { withMiddleware } from "~/lib/helpers/withMiddleware"; import { withMiddleware } from "~/lib/helpers/withMiddleware";
import { schemaQuerySingleOrMultipleUserEmails } from "~/lib/validations/shared/queryUserEmail";
import { schemaUsersReadPublic } from "~/lib/validations/user"; import { schemaUsersReadPublic } from "~/lib/validations/user";
/** /**
@@ -19,6 +20,17 @@ import { schemaUsersReadPublic } from "~/lib/validations/user";
* schema: * schema:
* type: string * type: string
* description: Your API key * 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: * tags:
* - users * - users
* responses: * responses:
@@ -39,6 +51,14 @@ export async function getHandler(req: NextApiRequest) {
const where: Prisma.UserWhereInput = {}; const where: Prisma.UserWhereInput = {};
// If user is not ADMIN, return only his data. // If user is not ADMIN, return only his data.
if (!isAdmin) where.id = userId; 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([ const [total, data] = await prisma.$transaction([
prisma.user.count({ where }), prisma.user.count({ where }),
prisma.user.findMany({ where, take, skip }), prisma.user.findMany({ where, take, skip }),
+21 -1
View File
@@ -1,9 +1,29 @@
import i18n from "i18next"; import i18n from "i18next";
import { initReactI18next } from "react-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({ i18n.use(initReactI18next).init({
resources: [],
debug: true, debug: true,
fallbackLng: "en",
defaultNS: "common",
ns,
interpolation: {
escapeValue: false,
},
react: { useSuspense: true },
resources,
}); });
export default i18n; export default i18n;
+8
View File
@@ -21,6 +21,14 @@ export const parameters = {
push() {}, push() {},
Provider: AppRouterContext.Provider, Provider: AppRouterContext.Provider,
}, },
globals: {
locale: "en",
locales: {
en: "English",
fr: "Français",
},
},
i18n,
}; };
addDecorator((storyFn) => ( addDecorator((storyFn) => (
+2 -1
View File
@@ -18,6 +18,7 @@ export function VariantsTable({
const columns = React.Children.toArray(children) as ReactElement<RowProps>[]; const columns = React.Children.toArray(children) as ReactElement<RowProps>[];
return ( return (
<div <div
id="light-variant"
className={classNames( className={classNames(
isDark && isDark &&
"relative py-8 before:absolute before:left-0 before:top-0 before:block before:h-full before:w-screen before:bg-[#1C1C1C]" "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> </table>
</div> </div>
{!isDark && ( {!isDark && (
<div data-mode="dark" className="dark"> <div id="dark-variant" data-mode="dark" className="dark">
<VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}> <VariantsTable titles={titles} isDark columnMinWidth={columnMinWidth}>
{children} {children}
</VariantsTable> </VariantsTable>
@@ -336,7 +336,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent> </DialogContent>
</Dialog> </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 <td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]" className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}> onClick={onClickTableData}>
@@ -35,6 +35,7 @@ import {
Tooltip, Tooltip,
} from "@calcom/ui"; } from "@calcom/ui";
import { Copy, Edit } from "@calcom/ui/components/icon"; import { Copy, Edit } from "@calcom/ui/components/icon";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import RequiresConfirmationController from "./RequiresConfirmationController"; import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -286,6 +287,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}> }}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */} {/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2"> <div className="lg:-ml-2">
{!IS_VISUAL_REGRESSION_TESTING && (
<TextField <TextField
disabled disabled
name="hashedLink" name="hashedLink"
@@ -316,6 +318,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</Tooltip> </Tooltip>
} }
/> />
)}
</div> </div>
</SettingsToggle> </SettingsToggle>
<hr className="border-subtle" /> <hr className="border-subtle" />
@@ -22,7 +22,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId; const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery( const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId }, { scheduleId: defaultScheduleId ?? undefined },
{ {
enabled: !!scheduleId, 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", "name": "@calcom/web",
"version": "3.2.2", "version": "3.2.4",
"private": true, "private": true,
"scripts": { "scripts": {
"analyze": "ANALYZE=true next build", "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"); const isSuccessPage = pathname?.startsWith("/booking");
-1
View File
@@ -53,7 +53,6 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
orgSlug: _orgSlug, orgSlug: _orgSlug,
...query ...query
} = useRouterQuery(); } = useRouterQuery();
const nameOrUsername = user.name || user.username || "";
/* /*
const telemetry = useTelemetry(); const telemetry = useTelemetry();
+1 -1
View File
@@ -51,7 +51,7 @@ class MyDocument extends Document<Props> {
<meta name="msapplication-TileColor" content="#ff0000" /> <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: light)" content="#f9fafb" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#1C1C1C" /> <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 // eslint-disable-next-line @next/next/no-sync-scripts
<script <script
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4" 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 username = slugify(data.username);
const userEmail = email.toLowerCase(); 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) { if (!username) {
res.status(422).json({ message: "Invalid username" }); res.status(422).json({ message: "Invalid username" });
return; return;
@@ -79,11 +66,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
} }
if (foundToken?.teamId) { if (foundToken?.teamId) {
const teamUserValidation = await validateUsernameInTeam(username, userEmail, 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 { } else {
const userValidation = await validateUsername(username, userEmail); 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); const hashedPassword = await hashPassword(password);
@@ -94,7 +85,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
id: foundToken.teamId, id: foundToken.teamId,
}, },
}); });
if (team) { if (team) {
const teamMetadata = teamMetadataSchema.parse(team?.metadata); 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) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end(); if (IS_PRODUCTION) return res.write("Only for development purposes"), res.end();
const t = await getTranslation("en", "common"); 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; res.statusCode = 200;
+2 -5
View File
@@ -2,7 +2,6 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { useRouter, useSearchParams, usePathname } from "next/navigation";
import { useCallback } from "react"; import { useCallback } from "react";
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
import { getLayout } from "@calcom/features/MainLayout"; import { getLayout } from "@calcom/features/MainLayout";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import { ShellMain } from "@calcom/features/shell/Shell"; import { ShellMain } from "@calcom/features/shell/Shell";
@@ -135,7 +134,6 @@ export default function AvailabilityPage() {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const orgBranding = useOrgBranding();
// Get a new searchParams string by merging the current // Get a new searchParams string by merging the current
// searchParams with a provided key/value pair // searchParams with a provided key/value pair
@@ -156,11 +154,11 @@ export default function AvailabilityPage() {
subtitle={t("configure_availability")} subtitle={t("configure_availability")}
CTA={ CTA={
<div className="flex gap-2"> <div className="flex gap-2">
{orgBranding && (
<ToggleGroup <ToggleGroup
className="hidden md:block" className="hidden md:block"
defaultValue={searchParams?.get("type") ?? "mine"} defaultValue={searchParams?.get("type") ?? "mine"}
onValueChange={(value) => { onValueChange={(value) => {
if (!value) return;
router.push(`${pathname}?${createQueryString("type", value)}`); router.push(`${pathname}?${createQueryString("type", value)}`);
}} }}
options={[ options={[
@@ -168,11 +166,10 @@ export default function AvailabilityPage() {
{ value: "team", label: t("team_availability") }, { value: "team", label: t("team_availability") },
]} ]}
/> />
)}
<NewScheduleButton /> <NewScheduleButton />
</div> </div>
}> }>
{searchParams?.get("type") === "team" && orgBranding ? ( {searchParams?.get("type") === "team" ? (
<AvailabilitySliderTable /> <AvailabilitySliderTable />
) : ( ) : (
<WithQuery <WithQuery
+1 -1
View File
@@ -179,7 +179,7 @@ export default function Bookings() {
)} )}
<div className="pt-2 xl:pt-0"> <div className="pt-2 xl:pt-0">
<div className="border-subtle overflow-hidden rounded-md border"> <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"> <tbody className="bg-default divide-subtle divide-y" data-testid="bookings">
{query.data.pages.map((page, index) => ( {query.data.pages.map((page, index) => (
<Fragment key={index}> <Fragment key={index}>
@@ -411,6 +411,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
throw new Error(t("seats_and_no_show_fee_error")); throw new Error(t("seats_and_no_show_fee_error"));
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input; const { availability, ...rest } = input;
updateMutation.mutate({ updateMutation.mutate({
...rest, ...rest,
@@ -495,6 +496,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
} }
} }
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input; const { availability, ...rest } = input;
updateMutation.mutate({ updateMutation.mutate({
...rest, ...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 type { CalPageWrapper } from "@components/PageWrapper";
import PageWrapper from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper";
const Page = UnverifiedOrgsPage as CalPageWrapper; const Page = AdminOrgsPage as CalPageWrapper;
Page.PageWrapper = PageWrapper; Page.PageWrapper = PageWrapper;
export default Page; export default Page;
+13 -11
View File
@@ -77,26 +77,25 @@ type FormValues = {
const ProfileView = () => { const ProfileView = () => {
const { t } = useLocale(); const { t } = useLocale();
const utils = trpc.useContext(); const utils = trpc.useContext();
const { data: _session, update } = useSession(); const { update } = useSession();
const { data: user, isLoading } = trpc.viewer.me.useQuery(); const { data: user, isLoading } = trpc.viewer.me.useQuery();
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({ const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async (res) => { onSuccess: async (res) => {
await update(res);
showToast(t("settings_updated_successfully"), "success"); showToast(t("settings_updated_successfully"), "success");
if (res.signOutUser && tempFormValues) {
if (res.passwordReset) { // signout user only in case of password reset
if (res.signOutUser && tempFormValues && res.passwordReset) {
showToast(t("password_reset_email", { email: tempFormValues.email }), "success"); 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" }); await signOut({ callbackUrl: "/auth/logout?passReset=true" });
} else { } else {
// sign out the user to avoid unauthorized access error
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
}
}
utils.viewer.me.invalidate(); utils.viewer.me.invalidate();
utils.viewer.avatar.invalidate(); utils.viewer.avatar.invalidate();
utils.viewer.shouldVerifyEmail.invalidate();
}
setConfirmAuthEmailChangeWarningDialogOpen(false); setConfirmAuthEmailChangeWarningDialogOpen(false);
update(res);
setTempFormValues(null); setTempFormValues(null);
}, },
onError: () => { onError: () => {
@@ -325,7 +324,10 @@ const ProfileView = () => {
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />} {confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
</div> </div>
<DialogFooter showDivider> <DialogFooter showDivider>
<Button color="primary" onClick={(e) => onConfirmPassword(e)}> <Button
color="primary"
loading={confirmPasswordMutation.isLoading}
onClick={(e) => onConfirmPassword(e)}>
{t("confirm")} {t("confirm")}
</Button> </Button>
<DialogClose /> <DialogClose />
@@ -345,7 +347,7 @@ const ProfileView = () => {
<DialogFooter> <DialogFooter>
<Button <Button
color="primary" color="primary"
disabled={updateProfileMutation.isLoading} loading={updateProfileMutation.isLoading}
onClick={(e) => onConfirmAuthEmailChange(e)}> onClick={(e) => onConfirmAuthEmailChange(e)}>
{t("confirm")} {t("confirm")}
</Button> </Button>
+11 -3
View File
@@ -199,19 +199,27 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
<SubTeams /> <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 && ( {!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl "> <div className="mx-auto max-w-3xl ">
<EventTypes /> <EventTypes />
{!team.hideBookATeamMember && ( {/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
{(!team.hideBookATeamMember || team.isPrivate) && (
<div> <div>
<div className="relative mt-12"> <div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true"> <div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="border-subtle w-full border-t" /> <div className="border-subtle w-full border-t" />
</div> </div>
<div className="relative flex justify-center"> <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")} {t("or")}
</span> </span>
</div> </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 { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants"; import { WEBAPP_URL } from "@calcom/lib/constants";
@@ -91,6 +92,7 @@ test.describe("Event Types tests", () => {
expect(formTitle).toBe(firstTitle); expect(formTitle).toBe(firstTitle);
expect(formSlug).toContain(firstSlug); expect(formSlug).toContain(firstSlug);
}); });
test("edit first event", async ({ page }) => { test("edit first event", async ({ page }) => {
const $eventTypes = page.locator("[data-testid=event-types] > li a"); const $eventTypes = page.locator("[data-testid=event-types] > li a");
const firstEventTypeElement = $eventTypes.first(); const firstEventTypeElement = $eventTypes.first();
@@ -152,5 +154,46 @@ test.describe("Event Types tests", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible(); 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, username: string | null,
eventTypeId = -1, eventTypeId = -1,
{ {
title = "",
rescheduled = false, rescheduled = false,
paid = false, paid = false,
status = "ACCEPTED", status = "ACCEPTED",
startTime,
endTime,
attendees = { attendees = {
create: { create: {
email: "attendee@example.com", email: "attendee@example.com",
@@ -39,9 +42,9 @@ export const createBookingsFixture = (page: Page) => {
const booking = await prisma.booking.create({ const booking = await prisma.booking.create({
data: { data: {
uid: uid, uid: uid,
title: "30min", title: title || "30min",
startTime: startDate, startTime: startTime || startDate,
endTime: endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(), endTime: endTime || endDateParam || dayjs().add(1, "day").add(30, "minutes").toDate(),
user: { user: {
connect: { connect: {
id: userId, id: userId,
+1 -1
View File
@@ -542,7 +542,7 @@ export async function apiLogin(
.then((json) => json.csrfToken); .then((json) => json.csrfToken);
const data = { const data = {
email: user.email ?? `${user.username}@example.com`, email: user.email ?? `${user.username}@example.com`,
password: user.password ?? user.username!, password: user.password ?? user.username,
callbackURL: "http://localhost:3000/", callbackURL: "http://localhost:3000/",
redirect: "false", redirect: "false",
json: "true", json: "true",
@@ -2010,6 +2010,21 @@
"member_removed": "Member removed", "member_removed": "Member removed",
"my_availability": "My Availability", "my_availability": "My Availability",
"team_availability": "Team 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", "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
@@ -2006,5 +2006,17 @@
"member_removed": "Membre supprimé", "member_removed": "Membre supprimé",
"my_availability": "Mes disponibilités", "my_availability": "Mes disponibilités",
"team_availability": "Disponibilités de l'équipe", "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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
} }
+32
View File
@@ -269,12 +269,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData); createBookingScenario(scenarioData);
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
},
}); });
// As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available // As per Google Calendar Availability, only 4PM(4-4:45PM) GMT slot would be available
@@ -353,6 +355,7 @@ describe("getSchedule", () => {
// Day Plus 2 is completely free - It only has non accepted bookings // Day Plus 2 is completely free - It only has non accepted bookings
const scheduleOnCompletelyFreeDay = await getSchedule({ const scheduleOnCompletelyFreeDay = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
// EventTypeSlug doesn't matter for non-dynamic events // EventTypeSlug doesn't matter for non-dynamic events
eventTypeSlug: "", eventTypeSlug: "",
@@ -360,6 +363,7 @@ describe("getSchedule", () => {
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
// getSchedule returns timeslots in GMT // getSchedule returns timeslots in GMT
@@ -384,12 +388,14 @@ describe("getSchedule", () => {
// Day plus 3 // Day plus 3
const scheduleForDayWithOneBooking = await getSchedule({ const scheduleForDayWithOneBooking = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`, startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`, endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForDayWithOneBooking).toHaveTimeSlots( expect(scheduleForDayWithOneBooking).toHaveTimeSlots(
@@ -448,12 +454,14 @@ describe("getSchedule", () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scheduleForEventWith30Length = await getSchedule({ const scheduleForEventWith30Length = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForEventWith30Length).toHaveTimeSlots( expect(scheduleForEventWith30Length).toHaveTimeSlots(
@@ -482,12 +490,14 @@ describe("getSchedule", () => {
); );
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({ const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
input: {
eventTypeId: 2, eventTypeId: 2,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
// `slotInterval` takes precedence over `length` // `slotInterval` takes precedence over `length`
// 4:30 is utc so it is 10:00 in IST // 4:30 is utc so it is 10:00 in IST
@@ -545,12 +555,14 @@ describe("getSchedule", () => {
const { dateString: todayDateString } = getDate(); const { dateString: todayDateString } = getDate();
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 }); const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
const scheduleForEventWithBookingNotice13Hrs = await getSchedule({ const scheduleForEventWithBookingNotice13Hrs = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`, startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`, endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots( expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
[ [
@@ -564,12 +576,14 @@ describe("getSchedule", () => {
); );
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({ const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
input: {
eventTypeId: 2, eventTypeId: 2,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${minus1DateString}T18:30:00.000Z`, startTime: `${minus1DateString}T18:30:00.000Z`,
endTime: `${todayDateString}T18:29:59.999Z`, endTime: `${todayDateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots( expect(scheduleForEventWithBookingNotice10Hrs).toHaveTimeSlots(
[ [
@@ -627,12 +641,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData); createBookingScenario(scenarioData);
const scheduleForEventOnADayWithNonCalBooking = await getSchedule({ const scheduleForEventOnADayWithNonCalBooking = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`, startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`, endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots( expect(scheduleForEventOnADayWithNonCalBooking).toHaveTimeSlots(
@@ -700,12 +716,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData); createBookingScenario(scenarioData);
const scheduleForEventOnADayWithCalBooking = await getSchedule({ const scheduleForEventOnADayWithCalBooking = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots( expect(scheduleForEventOnADayWithCalBooking).toHaveTimeSlots(
@@ -757,12 +775,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData); createBookingScenario(scenarioData);
const schedule = await getSchedule({ const schedule = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(schedule).toHaveTimeSlots( expect(schedule).toHaveTimeSlots(
@@ -820,12 +840,14 @@ describe("getSchedule", () => {
createBookingScenario(scenarioData); createBookingScenario(scenarioData);
const scheduleForEventOnADayWithDateOverride = await getSchedule({ const scheduleForEventOnADayWithDateOverride = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots( expect(scheduleForEventOnADayWithDateOverride).toHaveTimeSlots(
@@ -905,12 +927,14 @@ describe("getSchedule", () => {
// Requesting this user's availability for their // Requesting this user's availability for their
// individual Event Type // individual Event Type
const thisUserAvailability = await getSchedule({ const thisUserAvailability = await getSchedule({
input: {
eventTypeId: 2, eventTypeId: 2,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: false, isTeamEvent: false,
}
}); });
expect(thisUserAvailability).toHaveTimeSlots( expect(thisUserAvailability).toHaveTimeSlots(
@@ -1002,12 +1026,14 @@ describe("getSchedule", () => {
}); });
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({ const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${todayDateString}T18:30:00.000Z`, startTime: `${todayDateString}T18:30:00.000Z`,
endTime: `${plus1DateString}T18:29:59.999Z`, endTime: `${plus1DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: true, isTeamEvent: true,
}
}); });
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots( expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
@@ -1030,12 +1056,14 @@ describe("getSchedule", () => {
); );
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({ const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: true, isTeamEvent: true,
}
}); });
// A user with blocked time in another event, still affects Team Event availability // A user with blocked time in another event, still affects Team Event availability
@@ -1137,12 +1165,14 @@ describe("getSchedule", () => {
hosts: [], hosts: [],
}); });
const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({ const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`, startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`, endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: true, isTeamEvent: true,
}
}); });
// A user with blocked time in another event, still affects Team Event availability // A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots( expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots(
@@ -1163,12 +1193,14 @@ describe("getSchedule", () => {
); );
const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({ const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({
input: {
eventTypeId: 1, eventTypeId: 1,
eventTypeSlug: "", eventTypeSlug: "",
startTime: `${plus2DateString}T18:30:00.000Z`, startTime: `${plus2DateString}T18:30:00.000Z`,
endTime: `${plus3DateString}T18:29:59.999Z`, endTime: `${plus3DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"], timeZone: Timezones["+5:30"],
isTeamEvent: true, isTeamEvent: true,
}
}); });
// A user with blocked time in another event, still affects Team Event availability // A user with blocked time in another event, still affects Team Event availability
expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots( expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots(
@@ -303,6 +303,7 @@ describe("handleChildrenEventTypes", () => {
timeZone: _timeZone, timeZone: _timeZone,
parentId: _parentId, parentId: _parentId,
userId: _userId, userId: _userId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
requiresBookerEmailVerification, requiresBookerEmailVerification,
...evType ...evType
} = mockFindFirstEventType({ } = mockFindFirstEventType({
+44 -6
View File
@@ -40,6 +40,7 @@ type App = Partial<AppMeta> & {
function generateFiles() { function generateFiles() {
const browserOutput = [`import dynamic from "next/dynamic"`]; const browserOutput = [`import dynamic from "next/dynamic"`];
const metadataOutput = []; const metadataOutput = [];
const bookerMetadataOutput = [];
const schemasOutput = []; const schemasOutput = [];
const appKeysSchemasOutput = []; const appKeysSchemasOutput = [];
const serverOutput = []; 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++) { for (let i = 0; i < appDirs.length; i++) {
const configPath = path.join(APP_STORE_PATH, appDirs[i].path, "config.json"); 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; let app;
if (fs.existsSync(configPath)) { if (fs.existsSync(configPath)) {
app = JSON.parse(fs.readFileSync(configPath).toString()); 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 { } else {
app = {}; app = {};
} }
callback({ const finalApp = {
...app, ...app,
name: appDirs[i].name, name: appDirs[i].name,
path: appDirs[i].path, path: appDirs[i].path,
}); };
if (filter(finalApp)) {
callback(finalApp);
}
} }
} }
@@ -130,7 +139,8 @@ function generateFiles() {
lazyImport?: boolean; lazyImport?: boolean;
importConfig: ImportConfig; importConfig: ImportConfig;
entryObjectKeyGetter?: (arg: App, importName?: string) => string; entryObjectKeyGetter?: (arg: App, importName?: string) => string;
} },
filter?: (arg: App) => boolean
) { ) {
const output: string[] = []; const output: string[] = [];
@@ -174,7 +184,7 @@ function generateFiles() {
} }
} }
} }
}); }, filter);
} }
function createExportObject() { function createExportObject() {
@@ -201,7 +211,7 @@ function generateFiles() {
} }
} }
} }
}); }, filter);
output.push(`};`); 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( schemasOutput.push(
...getExportedObject("appDataSchemas", { ...getExportedObject("appDataSchemas", {
// Import path must have / even for windows and not \ // Import path must have / even for windows and not \
@@ -312,6 +341,7 @@ function generateFiles() {
["apps.browser.generated.tsx", browserOutput], ["apps.browser.generated.tsx", browserOutput],
["apps.schemas.generated.ts", schemasOutput], ["apps.schemas.generated.ts", schemasOutput],
["apps.keys-schemas.generated.ts", appKeysSchemasOutput], ["apps.keys-schemas.generated.ts", appKeysSchemasOutput],
["bookerApps.metadata.generated.ts", bookerMetadataOutput],
]; ];
filesToGenerate.forEach(([fileName, output]) => { filesToGenerate.forEach(([fileName, output]) => {
fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`)); fs.writeFileSync(`${APP_STORE_PATH}/${fileName}`, formatOutput(`${banner}${output.join("\n")}`));
@@ -347,3 +377,11 @@ if (isInWatchMode) {
} else { } else {
generateFiles(); 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 Script from "next/script";
import { getEventTypeAppData } from "@calcom/app-store/_utils/getEventTypeAppData"; 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"; import type { appDataSchemas } from "./apps.schemas.generated";
+2 -15
View File
@@ -1,7 +1,7 @@
import type { AppMeta } from "@calcom/types/App"; import type { AppMeta } from "@calcom/types/App";
import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated"; import { appStoreMetadata as rawAppStoreMetadata } from "./apps.metadata.generated";
import { getAppAssetFullPath } from "./getAppAssetFullPath"; import { getNormalizedAppMetadata } from "./getNormalizedAppMetadata";
type RawAppStoreMetaData = typeof rawAppStoreMetadata; type RawAppStoreMetaData = typeof rawAppStoreMetadata;
type AppStoreMetaData = { type AppStoreMetaData = {
@@ -10,18 +10,5 @@ type AppStoreMetaData = {
export const appStoreMetadata = {} as AppStoreMetaData; export const appStoreMetadata = {} as AppStoreMetaData;
for (const [key, value] of Object.entries(rawAppStoreMetadata)) { for (const [key, value] of Object.entries(rawAppStoreMetadata)) {
const dirName = "dirName" in value ? value.dirName : value.slug; appStoreMetadata[key as keyof typeof appStoreMetadata] = getNormalizedAppMetadata(value);
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,
});
} }
@@ -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 type { TFunction } from "next-i18next";
import { z } from "zod"; 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 logger from "@calcom/lib/logger";
import { BookingStatus } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums";
import type { Ensure, Optional } from "@calcom/types/utils"; 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 EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ eventType, app }) {
const { t } = useLocale(); const { t } = useLocale();
const [getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>(); const [_getAppData, setAppData, LockedIcon, disabled] = useAppContextWithSchema<typeof appDataSchema>();
const [additionalParameters, setAdditionalParameters] = useState(""); const [additionalParameters, setAdditionalParameters] = useState("");
const { enabled, updateEnabled } = useIsAppEnabled(app); const { enabled, updateEnabled } = useIsAppEnabled(app);
@@ -66,7 +66,6 @@ function Field({
hookForm, hookForm,
hookFieldNamespace, hookFieldNamespace,
deleteField, deleteField,
fieldIndex,
moveUp, moveUp,
moveDown, moveDown,
appUrl, appUrl,
@@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils"; import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server"; import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; 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 type { App_RoutingForms_Form } from "@prisma/client";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils"; import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server"; import { TRPCError } from "@calcom/trpc/server";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; 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 { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils";
import type { PrismaClient } from "@calcom/prisma";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { getSerializableForm } from "../lib/getSerializableForm"; import { getSerializableForm } from "../lib/getSerializableForm";
@@ -1,7 +1,8 @@
import { hasFilter } from "@calcom/features/filters/lib/hasFilter"; import { hasFilter } from "@calcom/features/filters/lib/hasFilter";
import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils"; import { entityPrismaWhereClause, canEditEntity } from "@calcom/lib/entityPermissionUtils";
import logger from "@calcom/lib/logger"; 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 { entries } from "@calcom/prisma/zod-utils";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; 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 logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server"; import { TRPCError } from "@calcom/trpc/server";
import { jsonLogicToPrisma } from "../jsonLogicToPrisma"; import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
@@ -1,7 +1,7 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { z } from "zod"; import { z } from "zod";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server"; import { TRPCError } from "@calcom/trpc/server";
import { getSerializableForm } from "../lib/getSerializableForm"; import { getSerializableForm } from "../lib/getSerializableForm";
@@ -3,7 +3,6 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { stringify } from "querystring"; import { stringify } from "querystring";
import createOAuthAppCredential from "../../_utils/createOAuthAppCredential"; import createOAuthAppCredential from "../../_utils/createOAuthAppCredential";
import { decodeOAuthState } from "../../_utils/decodeOAuthState";
import getInstalledAppPath from "../../_utils/getInstalledAppPath"; import getInstalledAppPath from "../../_utils/getInstalledAppPath";
import type { StripeData } from "../lib/server"; import type { StripeData } from "../lib/server";
import stripe 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" }); return res.status(401).json({ message: "You must be logged in to do this" });
} }
const state = decodeOAuthState(req);
const response = await stripe.oauth.token({ const response = await stripe.oauth.token({
grant_type: "authorization_code", grant_type: "authorization_code",
code: code?.toString(), code: code?.toString(),
@@ -9,7 +9,6 @@ import stripe from "../lib/server";
export default async function handler(req: NextApiRequest, res: NextApiResponse) { export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST" && req.method !== "GET") if (req.method !== "POST" && req.method !== "GET")
return res.status(405).json({ message: "Method not allowed" }); return res.status(405).json({ message: "Method not allowed" });
const { referer } = req.headers;
// if (!referer) return res.status(400).json({ message: "Missing referrer" }); // 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 type { EmailVerifyLink } from "./templates/account-verify-email";
import AccountVerifyEmail 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 AttendeeAwaitingPaymentEmail from "./templates/attendee-awaiting-payment-email";
import AttendeeCancelledEmail from "./templates/attendee-cancelled-email"; import AttendeeCancelledEmail from "./templates/attendee-cancelled-email";
import AttendeeCancelledSeatEmail from "./templates/attendee-cancelled-seat-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) => { export const sendOrganizationEmailVerification = async (sendOrgInput: OrganizationEmailVerify) => {
await sendEmail(() => new OrganizationEmailVerification(sendOrgInput)); 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 logger from "@calcom/lib/logger";
import type { CalendarEvent } from "@calcom/types/Calendar"; import type { CalendarEvent } from "@calcom/types/Calendar";
import { CallToActionIcon } from "./CallToActionIcon";
import { Info } from "./Info"; import { Info } from "./Info";
export function LocationInfo(props: { calEvent: CalendarEvent; t: TFunction }) { 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 { DailyVideoDownloadRecordingEmail } from "./DailyVideoDownloadRecordingEmail";
export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail"; export { OrganisationAccountVerifyEmail } from "./OrganizationAccountVerifyEmail";
export { OrgAutoInviteEmail } from "./OrgAutoInviteEmail"; 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 type { Page, Frame } from "@playwright/test";
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
// eslint-disable-next-line no-restricted-imports
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
export function todo(title: string) { export function todo(title: string) {
-1
View File
@@ -562,7 +562,6 @@ class CalApi {
modal({ modal({
calLink, calLink,
calOrigin,
config = {}, config = {},
uid, 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 dynamic from "next/dynamic";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import StickyBox from "react-sticky-box"; import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow"; import { shallow } from "zustand/shallow";
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager"; import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
import dayjs from "@calcom/dayjs";
import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useEmbedType, useEmbedUiConfig, useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { useNonEmptyScheduleDays } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames"; import classNames from "@calcom/lib/classNames";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils"; import { BookerLayouts, defaultBookerLayoutSettings } from "@calcom/prisma/zod-utils";
@@ -26,6 +28,7 @@ import { validateLayout } from "./utils/layout";
import { getQueryParam } from "./utils/query-param"; import { getQueryParam } from "./utils/query-param";
import { useBrandColors } from "./utils/use-brand-colors"; 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 PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity)); const UnpublishedEntity = dynamic(() => import("@calcom/ui").then((mod) => mod.UnpublishedEntity));
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), { 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 // 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' // 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 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 [bookerState, setBookerState] = useBookerStore((state) => [state.state, state.setState], shallow);
const selectedDate = useBookerStore((state) => state.selectedDate); const selectedDate = useBookerStore((state) => state.selectedDate);
@@ -86,9 +92,36 @@ const BookerComponent = ({
shallow 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 extraDays = isTablet ? extraDaysConfig[layout].tablet : extraDaysConfig[layout].desktop;
const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings; const bookerLayouts = event.data?.profile?.bookerLayouts || defaultBookerLayoutSettings;
const animationScope = useBookerResizeAnimation(layout, bookerState); 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. // 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 const defaultLayout = isEmbed
@@ -213,8 +246,9 @@ const BookerComponent = ({
)}> )}>
<Header <Header
enabledLayouts={bookerLayouts.enabledLayouts} enabledLayouts={bookerLayouts.enabledLayouts}
extraDays={extraDays} extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
isMobile={isMobile} isMobile={isMobile}
nextSlots={nextSlots}
/> />
</BookerSection> </BookerSection>
<StickyOnDesktop <StickyOnDesktop
@@ -280,7 +314,7 @@ const BookerComponent = ({
layout === BookerLayouts.COLUMN_VIEW layout === BookerLayouts.COLUMN_VIEW
} }
className={classNames( 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 && layout === BookerLayouts.MONTH_VIEW &&
"scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]", "scroll-bar h-full overflow-auto md:w-[var(--booker-timeslots-width)]",
layout !== BookerLayouts.MONTH_VIEW && "sticky top-0" layout !== BookerLayouts.MONTH_VIEW && "sticky top-0"
@@ -290,6 +324,8 @@ const BookerComponent = ({
<AvailableTimeSlots <AvailableTimeSlots
extraDays={extraDays} extraDays={extraDays}
limitHeight={layout === BookerLayouts.MONTH_VIEW} limitHeight={layout === BookerLayouts.MONTH_VIEW}
prefetchNextMonth={prefetchNextMonth}
monthCount={monthCount}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot} seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
/> />
</BookerSection> </BookerSection>
@@ -319,7 +355,7 @@ export const Booker = (props: BookerProps) => {
if (props.isAway) return <Away />; if (props.isAway) return <Away />;
return ( return (
<LazyMotion features={domAnimation}> <LazyMotion strict features={loadFramerFeatures}>
<BookerComponent {...props} /> <BookerComponent {...props} />
</LazyMotion> </LazyMotion>
); );
@@ -1,11 +1,14 @@
import { useMemo, useRef, useEffect } from "react"; import { useRef, useEffect } from "react";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { AvailableTimes, AvailableTimesSkeleton } from "@calcom/features/bookings"; 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 { classNames } from "@calcom/lib";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery"; import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { BookerLayouts } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc";
import { useBookerStore } from "../store"; import { useBookerStore } from "../store";
import { useEvent, useScheduleForEvent } from "../utils/event"; import { useEvent, useScheduleForEvent } from "../utils/event";
@@ -13,6 +16,8 @@ import { useEvent, useScheduleForEvent } from "../utils/event";
type AvailableTimeSlotsProps = { type AvailableTimeSlotsProps = {
extraDays?: number; extraDays?: number;
limitHeight?: boolean; limitHeight?: boolean;
prefetchNextMonth: boolean;
monthCount: number | undefined;
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
}; };
@@ -23,7 +28,14 @@ type AvailableTimeSlotsProps = {
* will also fetch the next `extraDays` days and show multiple days * will also fetch the next `extraDays` days and show multiple days
* in columns next to each other. * 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 isMobile = useMediaQuery("(max-width: 768px)");
const selectedDate = useBookerStore((state) => state.selectedDate); const selectedDate = useBookerStore((state) => state.selectedDate);
const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot); const setSelectedTimeslot = useBookerStore((state) => state.setSelectedTimeslot);
@@ -31,6 +43,8 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
const isEmbed = useIsEmbed(); const isEmbed = useIsEmbed();
const event = useEvent(); const event = useEvent();
const date = selectedDate || dayjs().format("YYYY-MM-DD"); 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 containerRef = useRef<HTMLDivElement | null>(null);
const onTimeSelect = ( const onTimeSelect = (
@@ -57,29 +71,23 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
}; };
const schedule = useScheduleForEvent({ 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. // Creates an array of dates to fetch slots for.
// If `extraDays` is passed in, we will extend the array with the next `extraDays` days. // If `extraDays` is passed in, we will extend the array with the next `extraDays` days.
const dates = useMemo( const dates = !extraDays
() =>
!extraDays
? [date] ? [date]
: [ : nonEmptyScheduleDaysFromSelectedDate.length > 0
// If NO date is selected yet, we show by default the upcomming `nextDays` days. ? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays)
date, : [];
...Array.from({ length: extraDays }).map((_, index) =>
dayjs(date)
.add(index + 1, "day")
.format("YYYY-MM-DD")
),
],
[date, extraDays]
);
const isMultipleDates = dates.length > 1; const slotsPerDay = useSlotsForAvailableDates(dates, schedule?.data?.slots);
const slotsPerDay = useSlotsForMultipleDates(dates, schedule?.data?.slots);
useEffect(() => { useEffect(() => {
if (isEmbed) return; if (isEmbed) return;
@@ -103,11 +111,16 @@ export const AvailableTimeSlots = ({ extraDays, limitHeight, seatsPerTimeSlot }:
<AvailableTimes <AvailableTimes
className="w-full" className="w-full"
key={slots.date} key={slots.date}
showTimeFormatToggle={!isColumnView}
onTimeSelect={onTimeSelect}
date={dayjs(slots.date)} date={dayjs(slots.date)}
slots={slots.slots} slots={slots.slots}
onTimeSelect={onTimeSelect}
seatsPerTimeSlot={seatsPerTimeSlot} seatsPerTimeSlot={seatsPerTimeSlot}
showTimeFormatToggle={!isMultipleDates} availableMonth={
dayjs(selectedDate).format("MM") !== dayjs(slots.date).format("MM")
? dayjs(slots.date).format("MMM")
: undefined
}
/> />
))} ))}
</div> </div>
@@ -63,7 +63,12 @@ export const EventMeta = () => {
)} )}
{!isLoading && !!event && ( {!isLoading && !!event && (
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}> <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> <EventTitle className="my-2">{event?.title}</EventTitle>
{event.description && ( {event.description && (
<EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4"> <EventMetaBlock contentClassName="mb-8 break-words max-w-full max-h-[180px] scroll-bar pr-4">
@@ -16,10 +16,12 @@ export function Header({
extraDays, extraDays,
isMobile, isMobile,
enabledLayouts, enabledLayouts,
nextSlots,
}: { }: {
extraDays: number; extraDays: number;
isMobile: boolean; isMobile: boolean;
enabledLayouts: BookerLayouts[]; enabledLayouts: BookerLayouts[];
nextSlots: number;
}) { }) {
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow); const [layout, setLayout] = useBookerStore((state) => [state.layout, state.setLayout], shallow);
@@ -56,8 +58,7 @@ export function Header({
if (isMonthView) { if (isMonthView) {
return <LayoutToggleWithData />; return <LayoutToggleWithData />;
} }
const endDate = selectedDate.add(layout === BookerLayouts.COLUMN_VIEW ? extraDays : extraDays - 1, "days");
const endDate = selectedDate.add(extraDays - 1, "days");
const isSameMonth = () => { const isSameMonth = () => {
return selectedDate.format("MMM") === endDate.format("MMM"); return selectedDate.format("MMM") === endDate.format("MMM");
@@ -91,7 +92,7 @@ export function Header({
color="minimal" color="minimal"
StartIcon={ChevronLeft} StartIcon={ChevronLeft}
aria-label="Previous Day" aria-label="Previous Day"
onClick={() => addToSelectedDate(-extraDays)} onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? -nextSlots : -extraDays)}
/> />
<Button <Button
className="group rtl:mr-1 rtl:rotate-180" className="group rtl:mr-1 rtl:rotate-180"
@@ -99,7 +100,7 @@ export function Header({
color="minimal" color="minimal"
StartIcon={ChevronRight} StartIcon={ChevronRight}
aria-label="Next Day" aria-label="Next Day"
onClick={() => addToSelectedDate(extraDays)} onClick={() => addToSelectedDate(layout === BookerLayouts.COLUMN_VIEW ? nextSlots : extraDays)}
/> />
{selectedDateMin3DaysDifference && ( {selectedDateMin3DaysDifference && (
<Button <Button
+1 -1
View File
@@ -224,7 +224,7 @@ export const extraDaysConfig = {
tablet: 4, tablet: 4,
}, },
[BookerLayouts.COLUMN_VIEW]: { [BookerLayouts.COLUMN_VIEW]: {
desktop: 4, desktop: 6,
tablet: 2, tablet: 2,
}, },
}; };
@@ -0,0 +1,3 @@
import { domAnimation } from "framer-motion";
export default domAnimation;
@@ -45,6 +45,7 @@ export const useScheduleForEvent = ({
eventId, eventId,
month, month,
duration, duration,
monthCount,
}: { }: {
prefetchNextMonth?: boolean; prefetchNextMonth?: boolean;
username?: string | null; username?: string | null;
@@ -52,6 +53,7 @@ export const useScheduleForEvent = ({
eventId?: number | null; eventId?: number | null;
month?: string | null; month?: string | null;
duration?: number | null; duration?: number | null;
monthCount?: number;
} = {}) => { } = {}) => {
const { timezone } = useTimePreferences(); const { timezone } = useTimePreferences();
const event = useEvent(); const event = useEvent();
@@ -70,6 +72,7 @@ export const useScheduleForEvent = ({
eventId: event.data?.id ?? eventId, eventId: event.data?.id ?? eventId,
timezone, timezone,
prefetchNextMonth, prefetchNextMonth,
monthCount,
rescheduleUid, rescheduleUid,
month: monthFromStore ?? month, month: monthFromStore ?? month,
duration: durationFromStore ?? duration, duration: durationFromStore ?? duration,
@@ -26,6 +26,7 @@ type AvailableTimesProps = {
seatsPerTimeSlot?: number | null; seatsPerTimeSlot?: number | null;
showTimeFormatToggle?: boolean; showTimeFormatToggle?: boolean;
className?: string; className?: string;
availableMonth?: string | undefined;
selectedSlots?: string[]; selectedSlots?: string[];
}; };
@@ -36,6 +37,7 @@ export const AvailableTimes = ({
seatsPerTimeSlot, seatsPerTimeSlot,
showTimeFormatToggle = true, showTimeFormatToggle = true,
className, className,
availableMonth,
selectedSlots, selectedSlots,
}: AvailableTimesProps) => { }: AvailableTimesProps) => {
const { t, i18n } = useLocale(); const { t, i18n } = useLocale();
@@ -65,6 +67,7 @@ export const AvailableTimes = ({
isMonthView ? "text-default text-sm" : "text-xs" isMonthView ? "text-default text-sm" : "text-xs"
)}> )}>
{date.format("DD")} {date.format("DD")}
{availableMonth && `, ${availableMonth}`}
</span> </span>
</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 { SchedulingType } from "@calcom/prisma/enums";
import { AvatarGroup } from "@calcom/ui"; import { AvatarGroup } from "@calcom/ui";
@@ -12,6 +13,7 @@ export interface EventMembersProps {
schedulingType: PublicEvent["schedulingType"]; schedulingType: PublicEvent["schedulingType"];
users: PublicEvent["users"]; users: PublicEvent["users"];
profile: PublicEvent["profile"]; profile: PublicEvent["profile"];
entity: PublicEvent["entity"];
} }
type Avatar = { type Avatar = {
@@ -23,7 +25,7 @@ type Avatar = {
type AvatarWithRequiredImage = Avatar & { image: string }; 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 showMembers = schedulingType !== SchedulingType.ROUND_ROBIN;
const shownUsers = showMembers ? users : []; const shownUsers = showMembers ? users : [];
@@ -37,9 +39,19 @@ export const EventMembers = ({ schedulingType, users, profile }: EventMembersPro
title: `${user.name}`, title: `${user.name}`,
image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`, image: "image" in user ? `${user.image}` : `/${user.username}/avatar.png`,
alt: user.name || undefined, 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. // Add profile later since we don't want to force creating an avatar for this if it doesn't exist.
avatars.unshift({ avatars.unshift({
title: `${profile.name}`, 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 type { z } from "zod";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import type { PrismaClient } from "@calcom/prisma";
import prisma from "@calcom/prisma"; import prisma from "@calcom/prisma";
type BookingSelect = { type BookingSelect = {
@@ -223,7 +223,7 @@ function preprocess<T extends z.ZodType>({
if (optionValue) { if (optionValue) {
// `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue` // `typeOfOptionInput` can be any of the main types. So, we the same validations should run for `optionValue`
if (typeOfOptionInput === "phone") { if (typeOfOptionInput === "phone") {
if (!phoneSchema.safeParse(optionValue).success) { if (!(await phoneSchema.safeParseAsync(optionValue)).success) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: m("invalid_number") }); 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 { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
import type { EventManagerUser } from "@calcom/core/EventManager"; 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 sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType"; import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
import logger from "@calcom/lib/logger"; import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums"; import { BookingStatus, WebhookTriggerEvents } from "@calcom/prisma/enums";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
@@ -107,7 +107,7 @@ export default function ApiKeyDialogForm({
: `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`} : `${t("expires")} ${apiKeyDetails?.expiresAt?.toLocaleDateString()}`}
</span> </span>
</div> </div>
<DialogFooter showDivider className="!mt-12"> <DialogFooter showDivider className="relative">
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}> <Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
{t("done")} {t("done")}
</Button> </Button>
@@ -209,7 +209,7 @@ export default function ApiKeyDialogForm({
</div> </div>
)} )}
<DialogFooter showDivider className="!mt-12"> <DialogFooter showDivider className="relative">
<Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}> <Button type="button" color="secondary" onClick={handleClose} tabIndex={-1}>
{t("cancel")} {t("cancel")}
</Button> </Button>
@@ -1,8 +1,8 @@
import type { PrismaClient } from "@prisma/client";
import cache from "memory-cache"; import cache from "memory-cache";
import { z } from "zod"; import { z } from "zod";
import { CONSOLE_URL } from "@calcom/lib/constants"; import { CONSOLE_URL } from "@calcom/lib/constants";
import type { PrismaClient } from "@calcom/prisma";
const CACHING_TIME = 86400000; // 24 hours in milliseconds 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) { export async function getDeploymentKey(prisma: PrismaClient) {
const deployment = await prisma.deployment.findUnique({ 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 short from "short-uuid";
import { v5 as uuidv5 } from "uuid"; import { v5 as uuidv5 } from "uuid";
import { sendSlugReplacementEmail } from "@calcom/emails/email-manager"; import { sendSlugReplacementEmail } from "@calcom/emails/email-manager";
import { getTranslation } from "@calcom/lib/server/i18n"; import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType } from "@calcom/prisma/enums"; import { SchedulingType } from "@calcom/prisma/enums";
import { _EventTypeModel } from "@calcom/prisma/zod"; import { _EventTypeModel } from "@calcom/prisma/zod";
import { allManagedEventTypeProps, unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils"; 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 }) { 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) { 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() { async onSuccess() {
await utils.viewer.teams.get.invalidate(); 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 { SAMLSSORecord, OIDCSSORecord } from "@boxyhq/saml-jackson";
import type { PrismaClient } from "@prisma/client";
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { isTeamAdmin } from "@calcom/lib/server/queries/teams"; import { isTeamAdmin } from "@calcom/lib/server/queries/teams";
import type { PrismaClient } from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server"; import { TRPCError } from "@calcom/trpc/server";
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || ""; 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({ const deleteTeamMutation = trpc.viewer.teams.delete.useMutation({
async onSuccess() { async onSuccess() {
await utils.viewer.teams.list.invalidate(); await utils.viewer.teams.list.invalidate();
await utils.viewer.teams.get.invalidate();
await utils.viewer.organizations.listMembers.invalidate(); await utils.viewer.organizations.listMembers.invalidate();
}, },
async onError(err) { async onError(err) {
@@ -11,6 +11,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownItem, DropdownItem,
DropdownMenuTrigger, DropdownMenuTrigger,
showToast,
} from "@calcom/ui"; } from "@calcom/ui";
import { Ban, Check, MoreHorizontal, X } from "@calcom/ui/components/icon"; 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({ const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: async () => { onSuccess: async () => {
showToast(t("success"), "success");
await utils.viewer.teams.get.invalidate(); await utils.viewer.teams.get.invalidate();
await utils.viewer.teams.hasTeamPlan.invalidate();
await utils.viewer.teams.list.invalidate(); await utils.viewer.teams.list.invalidate();
await utils.viewer.organizations.listMembers.invalidate(); await utils.viewer.organizations.listMembers.invalidate();
}, },
@@ -67,7 +67,10 @@ export default function TeamListItem(props: Props) {
const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({ const acceptOrLeaveMutation = trpc.viewer.teams.acceptOrLeave.useMutation({
onSuccess: () => { onSuccess: () => {
showToast(t("success"), "success");
utils.viewer.teams.get.invalidate();
utils.viewer.teams.list.invalidate(); utils.viewer.teams.list.invalidate();
utils.viewer.teams.hasTeamPlan.invalidate();
utils.viewer.teams.listInvites.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 { isRecurringEvent, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; 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 type { BookerLayoutSettings } from "@calcom/prisma/zod-utils";
import { import {
bookerLayoutOptions, bookerLayoutOptions,
+4 -2
View File
@@ -1,10 +1,12 @@
import type { AppFlags } from "@calcom/features/flags/config";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
export function useFlags() { export function useFlags() {
const query = trpc.viewer.features.map.useQuery(undefined, { const query = trpc.viewer.features.map.useQuery(undefined, {
initialData: process.env.NEXT_PUBLIC_IS_E2E initialData: process.env.NEXT_PUBLIC_IS_E2E
? { "managed-event-types": true, organizations: true, teams: true } ? { "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"; import type { AppFlags } from "../config";
@@ -10,6 +10,7 @@ type UseScheduleWithCacheArgs = {
timezone?: string | null; timezone?: string | null;
prefetchNextMonth?: boolean; prefetchNextMonth?: boolean;
duration?: number | null; duration?: number | null;
monthCount?: number | null;
rescheduleUid?: string | null; rescheduleUid?: string | null;
isTeamEvent?: boolean; isTeamEvent?: boolean;
}; };
@@ -22,11 +23,12 @@ export const useSchedule = ({
eventId, eventId,
prefetchNextMonth, prefetchNextMonth,
duration, duration,
monthCount,
rescheduleUid, rescheduleUid,
isTeamEvent, isTeamEvent,
}: UseScheduleWithCacheArgs) => { }: UseScheduleWithCacheArgs) => {
const monthDayjs = month ? dayjs(month) : dayjs(); 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, // 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 // and the query will not run if they are null. However, the check in `enabled` does
// no satisfy typscript. // no satisfy typscript.
@@ -7,6 +7,7 @@ import type { Slots } from "./types";
* @param date Format YYYY-MM-DD * @param date Format YYYY-MM-DD
* @param scheduleCache Instance of useScheduleWithCache * @param scheduleCache Instance of useScheduleWithCache
*/ */
export const useSlotsForDate = (date: string | null, slots?: Slots) => { export const useSlotsForDate = (date: string | null, slots?: Slots) => {
const slotsForDate = useMemo(() => { const slotsForDate = useMemo(() => {
if (!date || typeof slots === "undefined") return []; if (!date || typeof slots === "undefined") return [];
@@ -16,9 +17,9 @@ export const useSlotsForDate = (date: string | null, slots?: Slots) => {
return slotsForDate; return slotsForDate;
}; };
export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots) => { export const useSlotsForAvailableDates = (dates: (string | null)[], slots?: Slots) => {
const slotsForDates = useMemo(() => { const slotsForDates = useMemo(() => {
if (typeof slots === "undefined") return []; if (slots === undefined) return [];
return dates return dates
.filter((date) => date !== null) .filter((date) => date !== null)
.map((date) => ({ .map((date) => ({
@@ -26,6 +27,5 @@ export const useSlotsForMultipleDates = (dates: (string | null)[], slots?: Slots
date, date,
})); }));
}, [dates, slots]); }, [dates, slots]);
return slotsForDates; return slotsForDates;
}; };
@@ -5,6 +5,7 @@ import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale"; import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui"; import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
export default function TimezoneChangeDialog() { export default function TimezoneChangeDialog() {
const { t } = useLocale(); const { t } = useLocale();
@@ -43,7 +44,7 @@ export default function TimezoneChangeDialog() {
const tzDifferent = const tzDifferent =
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset(); !isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1"); const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
setOpen(showDialog); setOpen(!IS_VISUAL_REGRESSION_TESTING && showDialog);
}, [currentTz, isLoading, userTz]); }, [currentTz, isLoading, userTz]);
// save cookie to not show again // save cookie to not show again
+2 -1
View File
@@ -77,6 +77,7 @@ import {
Zap, Zap,
} from "@calcom/ui/components/icon"; } from "@calcom/ui/components/icon";
import { Discord } from "@calcom/ui/components/icon/Discord"; 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 { useOrgBranding } from "../ee/organizations/context/provider";
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider"; import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
@@ -920,7 +921,7 @@ function SideBar({ bannersHeight, user }: SideBarProps) {
</ButtonOrLink> </ButtonOrLink>
</Tooltip> </Tooltip>
))} ))}
<Credits /> {!IS_VISUAL_REGRESSION_TESTING && <Credits />}
</div> </div>
</aside> </aside>
</div> </div>
@@ -3,11 +3,14 @@ import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import { useMemo, useRef, useCallback, useEffect, useState } from "react"; import { useMemo, useRef, useCallback, useEffect, useState } from "react";
import dayjs from "@calcom/dayjs"; import dayjs from "@calcom/dayjs";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import type { DateRange } from "@calcom/lib/date-ranges"; import type { DateRange } from "@calcom/lib/date-ranges";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { MembershipRole } from "@calcom/prisma/enums"; import type { MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc"; import { trpc } from "@calcom/trpc";
import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui"; import { Avatar, Button, ButtonGroup, DataTable } from "@calcom/ui";
import { UpgradeTip } from "../../tips/UpgradeTip";
import { TBContext, createTimezoneBuddyStore } from "../store"; import { TBContext, createTimezoneBuddyStore } from "../store";
import { TimeDial } from "./TimeDial"; import { TimeDial } from "./TimeDial";
@@ -20,6 +23,32 @@ export interface SliderUser {
dateRanges: DateRange[]; 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() { export function AvailabilitySliderTable() {
const tableContainerRef = useRef<HTMLDivElement>(null); const tableContainerRef = useRef<HTMLDivElement>(null);
const [browsingDate, setBrowsingDate] = useState(dayjs()); const [browsingDate, setBrowsingDate] = useState(dayjs());
@@ -143,6 +172,9 @@ export function AvailabilitySliderTable() {
fetchMoreOnBottomReached(tableContainerRef.current); fetchMoreOnBottomReached(tableContainerRef.current);
}, [fetchMoreOnBottomReached]); }, [fetchMoreOnBottomReached]);
// This means they are not apart of any teams so we show the upgrade tip
if (!flatData.length) return <UpgradeTeamTip />;
return ( return (
<TBContext.Provider <TBContext.Provider
value={createTimezoneBuddyStore({ value={createTimezoneBuddyStore({
@@ -1,6 +1,5 @@
import type { PrismaClient } from "@prisma/client";
import defaultPrisma from "@calcom/prisma"; import defaultPrisma from "@calcom/prisma";
import type { PrismaClient } from "@calcom/prisma";
import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
export type GetSubscriberOptions = { 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 type { z } from "zod";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema"; import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import slugify from "@calcom/lib/slugify"; import slugify from "@calcom/lib/slugify";
import type { PrismaClient } from "@calcom/prisma";
type BookingSelect = { type BookingSelect = {
description: true; description: true;
+1 -1
View File
@@ -1,4 +1,3 @@
import type { PrismaClient } from "@prisma/client";
import { Prisma } from "@prisma/client"; import { Prisma } from "@prisma/client";
import { getLocationGroupedOptions } from "@calcom/app-store/server"; 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 { CAL_URL } from "@calcom/lib/constants";
import getPaymentAppData from "@calcom/lib/getPaymentAppData"; import getPaymentAppData from "@calcom/lib/getPaymentAppData";
import { getTranslation } from "@calcom/lib/server/i18n"; import { getTranslation } from "@calcom/lib/server/i18n";
import type { PrismaClient } from "@calcom/prisma";
import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums"; import { SchedulingType, MembershipRole, AppCategories } from "@calcom/prisma/enums";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; 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)); search.set(String(key), String(value));
router.replace(`${pathname}?${search.toString()}`); router.replace(`${pathname}?${search.toString()}`);
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps
[parsedQuery, router] [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) { export async function maybeGetBookingUidFromSeat(prisma: PrismaClient, uid: string) {
// Look bookingUid in bookingSeat // Look bookingUid in bookingSeat
+3 -1
View File
@@ -8,10 +8,12 @@ describe("slugify", () => {
expect(slugify("HELLO")).toEqual("hello"); 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("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", () => { 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 .normalize("NFD") // Normalize to decomposed form for handling accents
.replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters .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(/[^\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 start
.replace(/-+$/, "") // Remove dashes from end .replace(/-+$/, ""); // Remove dashes from end
.replace(/\+/g, "-"); // Transform all + to -
}; };
export default slugify; 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) { function isShiftEnter(node: HTMLElement) {
let currentNode: HTMLElement | null | ParentNode = node; let currentNode: HTMLElement | null | ParentNode = node;
+43 -6
View File
@@ -1,28 +1,65 @@
import type { Prisma } from "@prisma/client"; import type { Prisma } from "@prisma/client";
import { PrismaClient } from "@prisma/client"; import { PrismaClient as PrismaClientWithoutExtension } from "@prisma/client";
import { bookingReferenceMiddleware } from "./middleware"; import { bookingReferenceMiddleware } from "./middleware";
declare global { declare global {
// eslint-disable-next-line no-var // eslint-disable-next-line no-var
var prisma: PrismaClient | undefined; var prisma: typeof prismaWithClientExtensions;
} }
const prismaOptions: Prisma.PrismaClientOptions = {}; const prismaOptions: Prisma.PrismaClientOptions = {};
if (!!process.env.NEXT_PUBLIC_DEBUG) prismaOptions.log = ["query", "error", "warn"]; 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) => 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") { if (process.env.NODE_ENV !== "production") {
globalThis.prisma = prisma; globalThis.prisma = prisma;
} }
// If any changed on middleware server restart is required
bookingReferenceMiddleware(prisma);
export type PrismaClient = typeof prisma;
export default prisma; export default prisma;
export * from "./selects"; export * from "./selects";
@@ -1,4 +1,4 @@
import { PrismaClient } from "@prisma/client"; import type { PrismaClient } from "@prisma/client";
function middleware(prisma: PrismaClient) { function middleware(prisma: PrismaClient) {
/***********************************/ /***********************************/
+2
View File
@@ -79,7 +79,9 @@ model EventType {
bookingFields Json? bookingFields Json?
timeZone String? timeZone String?
periodType PeriodType @default(UNLIMITED) periodType PeriodType @default(UNLIMITED)
/// @zod.custom(imports.coerceToDate)
periodStartDate DateTime? periodStartDate DateTime?
/// @zod.custom(imports.coerceToDate)
periodEndDate DateTime? periodEndDate DateTime?
periodDays Int? periodDays Int?
periodCountCalendarDays Boolean? periodCountCalendarDays Boolean?
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff. Show more