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