Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4540c79580 | ||
|
|
00d01882d4 | ||
|
|
6ce4122528 | ||
|
|
7ddbcfcdcf | ||
|
|
b1b01ed383 | ||
|
|
6f5d9dc41f | ||
|
|
ac190b89dd | ||
|
|
990441295c | ||
|
|
1e4c20073c | ||
|
|
80353d67a0 | ||
|
|
d55b4b051a | ||
|
|
25d90e3e12 | ||
|
|
1727eb5625 | ||
|
|
58f4224c66 | ||
|
|
5d491e0a12 | ||
|
|
1eef5a56c3 | ||
|
|
d6740503be | ||
|
|
9f647ead46 | ||
|
|
f480afcace | ||
|
|
8d73d15b8a | ||
|
|
e8370ef950 | ||
|
|
e859c8dc58 | ||
|
|
b4b38b9bc3 | ||
|
|
d32719ad8b | ||
|
|
99d0816d9f | ||
|
|
95f4b89fe8 | ||
|
|
3e63c2b717 | ||
|
|
42cd56578d | ||
|
|
31d4cb4eb7 | ||
|
|
fde3e8bea9 | ||
|
|
874ad29676 | ||
|
|
5503d9d432 | ||
|
|
43b3d68447 | ||
|
|
e3c747a867 | ||
|
|
e020a2c9a4 | ||
|
|
e6e6f09547 | ||
|
|
68ec946c59 |
No files matched your search
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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) => (
|
||||||
|
|||||||
@@ -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,36 +287,38 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
}}>
|
}}>
|
||||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
{/* 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">
|
||||||
<TextField
|
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||||
disabled
|
<TextField
|
||||||
name="hashedLink"
|
disabled
|
||||||
label={t("private_link_label")}
|
name="hashedLink"
|
||||||
data-testid="generated-hash-url"
|
label={t("private_link_label")}
|
||||||
labelSrOnly
|
data-testid="generated-hash-url"
|
||||||
type="text"
|
labelSrOnly
|
||||||
hint={t("private_link_hint")}
|
type="text"
|
||||||
defaultValue={placeholderHashedLink}
|
hint={t("private_link_hint")}
|
||||||
addOnSuffix={
|
defaultValue={placeholderHashedLink}
|
||||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
addOnSuffix={
|
||||||
<Button
|
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||||
color="minimal"
|
<Button
|
||||||
size="sm"
|
color="minimal"
|
||||||
type="button"
|
size="sm"
|
||||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
type="button"
|
||||||
aria-label="copy link"
|
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||||
onClick={() => {
|
aria-label="copy link"
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
onClick={() => {
|
||||||
if (eventType.hashedLink) {
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
showToast(t("private_link_copied"), "success");
|
if (eventType.hashedLink) {
|
||||||
} else {
|
showToast(t("private_link_copied"), "success");
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
} else {
|
||||||
}
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
}}>
|
}
|
||||||
<Copy className="h-4 w-4" />
|
}}>
|
||||||
</Button>
|
<Copy className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Button>
|
||||||
}
|
</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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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,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,23 +154,22 @@ 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={[
|
||||||
{ value: "mine", label: t("my_availability") },
|
{ value: "mine", label: t("my_availability") },
|
||||||
{ 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
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
|
if (res.signOutUser && tempFormValues && res.passwordReset) {
|
||||||
// sign out the user to avoid unauthorized access error
|
showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
|
||||||
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
|
utils.viewer.me.invalidate();
|
||||||
await signOut({ callbackUrl: "/auth/logout?emailChange=true" });
|
utils.viewer.avatar.invalidate();
|
||||||
}
|
utils.viewer.shouldVerifyEmail.invalidate();
|
||||||
}
|
}
|
||||||
utils.viewer.me.invalidate();
|
|
||||||
utils.viewer.avatar.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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 ?? "");
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||||
}
|
}
|
||||||
@@ -269,12 +269,14 @@ describe("getSchedule", () => {
|
|||||||
createBookingScenario(scenarioData);
|
createBookingScenario(scenarioData);
|
||||||
|
|
||||||
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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,13 +355,15 @@ 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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
// EventTypeSlug doesn't matter for non-dynamic events
|
eventTypeId: 1,
|
||||||
eventTypeSlug: "",
|
// EventTypeSlug doesn't matter for non-dynamic events
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
isTeamEvent: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
expect(scheduleForEventWith30Length).toHaveTimeSlots(
|
||||||
@@ -482,12 +490,14 @@ describe("getSchedule", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
|
const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({
|
||||||
eventTypeId: 2,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 2,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
isTeamEvent: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
expect(scheduleForEventWithBookingNotice13Hrs).toHaveTimeSlots(
|
||||||
[
|
[
|
||||||
@@ -564,12 +576,14 @@ describe("getSchedule", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
|
const scheduleForEventWithBookingNotice10Hrs = await getSchedule({
|
||||||
eventTypeId: 2,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 2,
|
||||||
startTime: `${minus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${todayDateString}T18:29:59.999Z`,
|
startTime: `${minus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${todayDateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 2,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 2,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: false,
|
timeZone: Timezones["+5:30"],
|
||||||
|
isTeamEvent: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(thisUserAvailability).toHaveTimeSlots(
|
expect(thisUserAvailability).toHaveTimeSlots(
|
||||||
@@ -1002,12 +1026,14 @@ describe("getSchedule", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${todayDateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus1DateString}T18:29:59.999Z`,
|
startTime: `${todayDateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus1DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: true,
|
timeZone: Timezones["+5:30"],
|
||||||
|
isTeamEvent: true,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots(
|
||||||
@@ -1030,12 +1056,14 @@ describe("getSchedule", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
|
const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: true,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: true,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
eventTypeId: 1,
|
input: {
|
||||||
eventTypeSlug: "",
|
eventTypeId: 1,
|
||||||
startTime: `${plus2DateString}T18:30:00.000Z`,
|
eventTypeSlug: "",
|
||||||
endTime: `${plus3DateString}T18:29:59.999Z`,
|
startTime: `${plus2DateString}T18:30:00.000Z`,
|
||||||
timeZone: Timezones["+5:30"],
|
endTime: `${plus3DateString}T18:29:59.999Z`,
|
||||||
isTeamEvent: true,
|
timeZone: Timezones["+5:30"],
|
||||||
|
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({
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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,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" });
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -562,7 +562,6 @@ class CalApi {
|
|||||||
|
|
||||||
modal({
|
modal({
|
||||||
calLink,
|
calLink,
|
||||||
calOrigin,
|
|
||||||
config = {},
|
config = {},
|
||||||
uid,
|
uid,
|
||||||
}: {
|
}: {
|
||||||
|
|||||||
@@ -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
|
||||||
() =>
|
? [date]
|
||||||
!extraDays
|
: nonEmptyScheduleDaysFromSelectedDate.length > 0
|
||||||
? [date]
|
? nonEmptyScheduleDaysFromSelectedDate.slice(0, extraDays)
|
||||||
: [
|
: [];
|
||||||
// If NO date is selected yet, we show by default the upcomming `nextDays` days.
|
|
||||||
date,
|
|
||||||
...Array.from({ length: extraDays }).map((_, index) =>
|
|
||||||
dayjs(date)
|
|
||||||
.add(index + 1, "day")
|
|
||||||
.format("YYYY-MM-DD")
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[date, extraDays]
|
|
||||||
);
|
|
||||||
|
|
||||||
const 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
|
||||||
|
|||||||
@@ -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,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,
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
/***********************************/
|
/***********************************/
|
||||||
|
|||||||
@@ -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
Reference in new issue
Block a user