Compare commits

...
14 Commits
Author SHA1 Message Date
Omar López d7aa3c7163 v2.7.8
release.yml / release (push) Canceled after 0s
2023-04-04 16:58:13 -07:00
Peer Richelsen e899a92071 added turkish (#8091) 2023-04-05 00:13:00 +02:00
zomars ebdbe407cd Fixes crowding update 2023-04-04 12:43:00 -07:00
e7f917bab9 Feedback and improvements (#7903)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-04 10:35:51 -07:00
Hariom Balhara ba9688a04a BugFix: Don't open location dialog on removing location (#8085) 2023-04-04 14:43:41 +00:00
Stefan Florea f0e15d77dc feat: Google Tag Manager (GTM) App (#7952) 2023-04-04 17:53:11 +05:30
30b72b687e Follow up insights - user metrics (#8016)
* init page

* init insights frontend

* nit

* nit

* nit

* merge

* fixed icons

* i18n, needs features

* Init insights trpc

* Using trpc on client

* Added events timeline

* Seed analytics script

* connect ui with trpc

* Added and fixed event time lines

* WIP popular days and avg time duration event type

* added new metric graphs

* improved upgrade tip

* always show upgrade screen

* upgrade tremor.so and select inputs for page

* Remove log

* Move everything to components and add context

* Fix select types using calcom ui one

* Adding translations

* Add missing translations

* Add more translations

* min fix

* Fixes for date select

* Prefer early return and mobile design fixes

* Fix style for mobile

* Fix data with userId filter from popular events

* add user id to average time duration

* fix types for select-react

* Removed submodules

* Delete website

* Update yarn.lock

* Code organization and type fixes

* trpc fixes

* Builds are now passing

* Relocates server code

* Add url state in insights

* Update FiltersProvider.tsx

* Cleanup

* Update embed-iframe.ts

* Update FilterType.tsx

* Update seed-app-store.config.json

* Update index.tsx

* Renamed seeder

* Update FiltersProvider.tsx

* Fix for query params

* no wrap on lg screen

* Fix shadow borders from tremor components, fix title font

* Add ring-gray to match filters

* add cursor pointer

* copy improvements

* blue to black

* fixed date select focus

* text right for table

* Adds missing translation strings

* Fix url state for filter type

* more layout improvements

* more padding improvements

* more UI fixes (padding, sizes) requested by ciaran

* Apply suggestions from code review

* WIP user without team metrics

* yarn

* Adds loading state for graphs and tables

* fix average render when no data

* Multiple fixes in insights

* Fix most and least booked members querys

* More fix for clear filters

---------

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-04-04 13:58:19 +02:00
Peer Richelsenandsean-brydon bbf50dbc53 fixed links and more on signup 404 page that requires license (#7937)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-04-04 11:49:44 +02:00
Ted SpareandPeer Richelsen bb547a0111 Apply labels from linked issues to a PR (#8069)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-04 09:28:36 +00:00
Hariom BalharaandAlex van Andel 1758288948 Improvement: Do immediate redirect for "Redirect on Booking" (#7947)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2023-04-04 14:22:27 +05:30
Udit Takkar 11789c725a fix: remove box shadow none (#8077)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-04-04 08:06:11 +00:00
Hariom Balhara 73bfe0aebe HotFix: Broken Email Field in Routing Form (#8074)
* Add darkmode styles and make it full width and other classes that are for TextWidget

* Handle email text change
2023-04-04 07:55:56 +00:00
Harold ThétiotandHariom Balhara 24140f0f29 feat: add sylaps into the app-store in video category (#6728)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-04-04 13:12:07 +05:30
e504f5cd9e Fix/send responses everywhere customInputs were being sent. (#7898)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-04-04 10:29:09 +05:30
76 changed files with 2143 additions and 706 deletions
@@ -0,0 +1,27 @@
name: "Apply issue labels to PR"
on:
pull_request:
types: [opened]
jobs:
label_on_pr:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 16
- name: Apply labels from linked issue to PR
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_DATA: ${{ toJson(github.event.pull_request) }}
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
+1 -1
View File
@@ -26,6 +26,6 @@ jobs:
localization_branch_name: main
create_pull_request: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
@@ -0,0 +1,149 @@
const https = require("https");
async function applyLabelFromLinkedIssueToPR(pr, token) {
// Get the labels from issues linked to the PR
const query = `
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
closingIssuesReferences(first: 10) {
nodes {
number
labels(first: 10) {
nodes {
name
}
}
}
}
}
}
}
`;
const graphqlData = JSON.stringify({
query,
variables: {
owner: pr.base.repo.owner.login,
repo: pr.base.repo.name,
prNumber: pr.number,
},
});
const requestOptions = {
hostname: "api.github.com",
path: "/graphql",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": graphqlData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
// Use the native Node.js request library to make the request
let linkedIssues;
linkedIssues = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("end", () => {
resolve(
JSON.parse(responseBody)
?.data
?.repository
?.pullRequest
?.closingIssuesReferences
?.nodes
);
});
});
request.on("error", (error) => {
reject(error);
});
request.write(graphqlData);
request.end();
});
if (!linkedIssues || linkedIssues.length === 0) {
console.log("No issue linked.");
return;
}
// Iterate over linked issues and apply labels to PR
for (const issue of linkedIssues) {
const labels = issue?.labels?.nodes?.map((label) => label.name);
if (!labels || labels.length === 0) {
console.log(`No labels found on the linked issue #${issue.number}.`);
continue;
}
const labelsData = JSON.stringify({
labels: labels,
});
const requestOptions = {
hostname: "api.github.com",
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": labelsData.length,
"Authorization": "Bearer " + token,
"User-Agent": "Node.js",
},
};
let labelResult;
labelResult = await new Promise((resolve, reject) => {
const request = https.request(requestOptions, (response) => {
let responseBody = "";
response.on("data", (chunk) => {
responseBody += chunk;
});
response.on("data", () => {});
response.on("end", () => {
resolve(JSON.parse(responseBody));
});
});
request.on("error", (error) => {
reject(error);
});
request.write(labelsData);
request.end();
});
if (labelResult.message) {
console.log(`Error labelling PR: ${labelResult.message}`);
continue;
}
console.log(
`Applied labels: ${labels.join(", ")} to PR #${
pr.number
} from linked issue #${issue.number}`
);
}
}
// Pass the PR data and GitHub token to the script
(async () => {
if (!process.env.PR_DATA) {
console.log("No PR data found.");
return;
}
const prData = JSON.parse(process.env.PR_DATA);
const token = process.env.GITHUB_TOKEN;
await applyLabelFromLinkedIssueToPR(prData, token);
})();
@@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { classNames } from "@calcom/lib";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
setRedirectUrlVisible(e);
onChange(e ? value : "");
}}>
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
<div className="lg:-ml-2">
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
<div className="lg:-ml-2 lg:-mb-2">
<TextField
className="w-full"
label={t("redirect_success_booking")}
labelSrOnly
placeholder={t("external_redirect_url")}
@@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div className="mt-2 flex">
<Checkbox
description={t("disable_success_page")}
// Disable if it's not Self Hosted or if the redirect url is not set
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
{...formMethods.register("metadata.disableSuccessPage")}
/>
{/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing" target="_blank">
<Badge variant="orange" className="ml-2">
Platform Only
</Badge>
</Link>
)}
<div
className={classNames(
"p-1 text-sm text-orange-600",
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
)}>
{t("redirect_url_warning")}
</div>
</div>
</SettingsToggle>
@@ -229,14 +229,6 @@ export const EventSetupTab = (
return (
<li
onClick={() => {
locationFormMethods.setValue("locationType", location.type);
locationFormMethods.unregister("locationLink");
locationFormMethods.unregister("locationAddress");
locationFormMethods.unregister("locationPhoneNumber");
setEditingLocationType(location.type);
openLocationModal(location.type);
}}
key={`${location.type}${index}`}
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
<div className="flex items-center justify-between">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.7.7",
"version": "2.7.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+4 -4
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HeadSeo } from "@calcom/ui";
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
@@ -99,7 +99,7 @@ export default function Custom404() {
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
{t("signup_requires")}
</h1>
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p>
<p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
</div>
<div className="mt-12">
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
@@ -136,7 +136,7 @@ export default function Custom404() {
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
<li className="px-4 py-2">
<Link
href={`${DEVELOPER_DOCS}/self-hosting/installation`}
href="https://cal.com/self-hosting/installation"
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
<div className="flex-shrink-0">
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
@@ -205,7 +205,7 @@ export default function Custom404() {
</ul>
<div className="mt-8">
<Link
href={`${WEBSITE_URL}/enterprise`}
href="https://cal.com/sales"
className="text-base font-medium text-black hover:text-gray-500">
{t("contact_sales")}
<span aria-hidden="true"> &rarr;</span>
+2 -2
View File
@@ -13,6 +13,8 @@ import {
getGroupName,
getUsernameList,
} from "@calcom/lib/defaultEvents";
import getBooking from "@calcom/lib/getBooking";
import type { GetBookingType } from "@calcom/lib/getBooking";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
@@ -22,8 +24,6 @@ import {
userMetadata as userMetadataSchema,
} from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server/i18n";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
@@ -48,8 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
eventType: {
select: {
recurringEvent: true,
bookingFields: true,
},
},
responses: true,
uid: true,
destinationCalendar: true,
},
@@ -96,6 +99,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
title: booking.title,
description: booking.description || undefined,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: booking.location ?? "",
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
+8 -125
View File
@@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { useEffect, useState } from "react";
import { RRule } from "rrule";
import { z } from "zod";
@@ -36,6 +36,7 @@ import {
formatToLocalizedTimezone,
} from "@calcom/lib/date-fns";
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
import { getBookingWithResponses } from "@calcom/lib/getBooking";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
@@ -51,7 +52,6 @@ import { Button, EmailInput, HeadSeo, Badge } from "@calcom/ui";
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
import { timeZone } from "@lib/clock";
import { getBookingWithResponses } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
@@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { ssrInit } from "@server/lib/ssr";
function redirectToExternalUrl(url: string) {
window.parent.location.href = url;
}
/**
* Redirects to external URL with query params from current URL.
* Query Params and Hash Fragment if present in external URL are kept intact.
*/
function RedirectionToast({ url }: { url: string }) {
const [timeRemaining, setTimeRemaining] = useState(10);
const [isToastVisible, setIsToastVisible] = useState(true);
const { t } = useLocale();
const timerRef = useRef<number | null>(null);
const router = useRouter();
const { cancel: isCancellationMode } = querySchema.parse(router.query);
const urlWithSuccessParamsRef = useRef<string | null>();
if (isCancellationMode && timerRef.current) {
setIsToastVisible(false);
}
useEffect(() => {
if (!isToastVisible && timerRef.current) {
window.clearInterval(timerRef.current);
}
}, [isToastVisible]);
useEffect(() => {
const parsedExternalUrl = new URL(url);
const parsedSuccessUrl = new URL(document.URL);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
parsedSuccessUrl.searchParams.set(name, value);
}
const urlWithSuccessParams =
parsedExternalUrl.origin +
parsedExternalUrl.pathname +
"?" +
parsedSuccessUrl.searchParams.toString() +
parsedExternalUrl.hash;
urlWithSuccessParamsRef.current = urlWithSuccessParams;
timerRef.current = window.setInterval(() => {
if (timeRemaining > 0) {
setTimeRemaining((timeRemaining) => {
return timeRemaining - 1;
});
} else {
redirectToExternalUrl(urlWithSuccessParams);
window.clearInterval(timerRef.current as number);
}
}, 1000);
return () => {
window.clearInterval(timerRef.current as number);
};
}, [timeRemaining, url]);
if (!isToastVisible) {
return null;
}
return (
<>
<div className="relative z-[60] pb-2 sm:pb-5">
<div className="mx-auto w-full sm:max-w-7xl sm:px-2 lg:px-8">
<div className="border border-green-600 bg-green-500 p-2 sm:p-3">
<div className="flex flex-wrap items-center justify-between">
<div className="flex w-0 flex-1 items-center">
<p className="truncate font-medium text-white sm:mx-3">
<span className="md:hidden">Redirecting to {url} ...</span>
<span className="hidden md:inline">
{t("you_are_being_redirected", { url, seconds: timeRemaining })}
</span>
</p>
</div>
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
<button
onClick={() => {
if (urlWithSuccessParamsRef.current) {
redirectToExternalUrl(urlWithSuccessParamsRef.current);
}
}}
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
{t("continue")}
</button>
</div>
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
<button
type="button"
onClick={() => {
setIsToastVisible(false);
}}
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
<FiX className="h-6 w-6 text-white" />
</button>
</div>
</div>
</div>
</div>
</div>
</>
);
}
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
const stringToBoolean = z
@@ -176,13 +68,14 @@ const stringToBoolean = z
const querySchema = z.object({
uid: z.string(),
allRemainingBookings: stringToBoolean,
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
allRemainingBookings: stringToBoolean,
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: stringToBoolean,
formerTime: z.string().optional(),
email: z.string().optional(),
seatReferenceUid: z.string().optional(),
});
@@ -404,9 +297,6 @@ export default function Success(props: SuccessProps) {
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
<RedirectionToast url={eventType.successRedirectUrl} />
) : null}{" "}
<div
className={classNames(
shouldAlignCentrally ? "text-center" : "",
@@ -981,13 +871,6 @@ const getEventTypesFromDB = async (id: number) => {
};
};
const schema = z.object({
uid: z.string(),
email: z.string().optional(),
eventTypeSlug: z.string().optional(),
cancel: stringToBoolean,
});
const handleSeatsEventTypeOnBooking = (
eventType: {
seatsPerTimeSlot?: number | null;
@@ -1017,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
export async function getServerSideProps(context: GetServerSidePropsContext) {
const ssr = await ssrInit(context);
const parsedQuery = schema.safeParse(context.query);
const parsedQuery = querySchema.safeParse(context.query);
if (!parsedQuery.success) return { notFound: true };
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
const bookingInfoRaw = await prisma.booking.findFirst({
where: {
@@ -1116,7 +999,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
return {
redirect: {
destination: eventType.successRedirectUrl,
+1 -1
View File
@@ -5,12 +5,12 @@ import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import type { GetBookingType } from "@calcom/lib/getBooking";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import type { GetBookingType } from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
+6 -11
View File
@@ -8,7 +8,6 @@ import {
PopularEventsTable,
} from "@calcom/features/insights/components";
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
import { useFilterContext } from "@calcom/features/insights/context/provider";
import { Filters } from "@calcom/features/insights/filters";
import Shell from "@calcom/features/shell/Shell";
import { UpgradeTip } from "@calcom/features/tips";
@@ -20,15 +19,11 @@ import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
const Heading = () => {
const { t } = useLocale();
const {
filter: { selectedTeamName },
} = useFilterContext();
return (
<div className="min-w-52">
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden truncate text-xl font-semibold tracking-wide text-black md:block xl:max-w-full">
{t("analytics_for_organisation", {
organisationName: selectedTeamName,
})}
<div className="min-w-52 hidden md:block">
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 truncate text-xl font-semibold tracking-wide text-black xl:max-w-full">
{t("analytics_for_organisation")}
</h3>
<p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p>
</div>
@@ -80,7 +75,7 @@ export default function InsightsPage() {
<></>
) : (
<FiltersProvider>
<div className="mb-4 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
<div className="mb-8 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
<Heading />
<Filters />
</div>
@@ -99,7 +94,7 @@ export default function InsightsPage() {
<LeastBookedTeamMembersTable />
</div>
<small className="block text-center text-gray-600">
{t("looking_for_more_analytics")}
{t("looking_for_more_insights")}
<a
className="text-blue-500 hover:underline"
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
+2 -2
View File
@@ -4,13 +4,13 @@ import type { LocationObject } from "@calcom/core/location";
import { privacyFilteredLocations } from "@calcom/core/location";
import { parseRecurringEvent } from "@calcom/lib";
import { getWorkingHours } from "@calcom/lib/availability";
import getBooking from "@calcom/lib/getBooking";
import type { GetBookingType } from "@calcom/lib/getBooking";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
+2 -2
View File
@@ -5,13 +5,13 @@ import type { LocationObject } from "@calcom/app-store/locations";
import { privacyFilteredLocations } from "@calcom/app-store/locations";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { parseRecurringEvent } from "@calcom/lib";
import type { GetBookingType } from "@calcom/lib/getBooking";
import getBooking from "@calcom/lib/getBooking";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import prisma from "@calcom/prisma";
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
import type { GetBookingType } from "@lib/getBooking";
import getBooking from "@lib/getBooking";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import BookingPage from "@components/booking/pages/BookingPage";
@@ -404,7 +404,7 @@
"add_time_availability": "Add new time slot",
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
"2fa": "Two-Factor Authentication",
"2fa_disabled":"Two-Factor authentication can only be enabled for email and password authentication",
"2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication",
"enable_2fa": "Enable two-factor authentication",
"disable_2fa": "Disable two-factor authentication",
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
@@ -1673,7 +1673,7 @@
"default_conferencing_bulk_title": "Bulk update existing event types",
"requires_at_least_one_schedule": "You are required to have at least one schedule",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"looking_for_more_analytics": "Looking for more analytics?",
"looking_for_more_insights": "Looking for more Insights?",
"add_filter": "Add filter",
"select_user": "Select User",
"select_event_type": "Select Event Type",
@@ -1689,10 +1689,12 @@
"events_rescheduled": "Events Rescheduled",
"from_last_period": "from last period",
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
"analytics_for_organisation": "Insights for {{organisationName}}",
"analytics_for_organisation": "Insights",
"subtitle_analytics": "Learn more about your team's activity",
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
"event_trends": "Event Trends",
"clear_filters": "Clear Filters",
"insights": "Insights",
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
"email_invite_team": "{{email}} has been invited"
}
-2
View File
@@ -148,8 +148,6 @@ select:focus {
--tw-ring-color: var(--brand-color);
border-color: var(--brand-color);
}
/* https://stackoverflow.com/a/69308190 */
input[type='text']:focus { box-shadow: none; }
@layer components {
@@ -40,6 +40,7 @@ export const EventTypeAddonMap = {
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")),
@@ -6,6 +6,7 @@ import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
import { appKeysSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appKeysSchemas = {
fathom: fathom_zod_ts,
ga4: ga4_zod_ts,
giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ 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 giphy__metadata_ts } from "./giphy/_metadata";
import google_tag_manager_config_json from "./google-tag-manager/config.json";
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
@@ -39,6 +40,7 @@ import sendgrid_config_json from "./sendgrid/config.json";
import signal_config_json from "./signal/config.json";
import sirius_video_config_json from "./sirius_video/config.json";
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
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 basic_config_json from "./templates/basic/config.json";
@@ -75,6 +77,7 @@ export const appStoreMetadata = {
fathom: fathom_config_json,
ga4: ga4_config_json,
giphy: giphy__metadata_ts,
"google-tag-manager": google_tag_manager_config_json,
googlecalendar: googlecalendar__metadata_ts,
googlevideo: googlevideo__metadata_ts,
hubspot: hubspot__metadata_ts,
@@ -97,6 +100,7 @@ export const appStoreMetadata = {
signal: signal_config_json,
sirius_video: sirius_video_config_json,
stripepayment: stripepayment__metadata_ts,
sylapsvideo: sylapsvideo_config_json,
tandemvideo: tandemvideo__metadata_ts,
telegram: telegram_config_json,
basic: basic_config_json,
@@ -6,6 +6,7 @@ import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
import { appDataSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
@@ -30,6 +31,7 @@ export const appDataSchemas = {
fathom: fathom_zod_ts,
ga4: ga4_zod_ts,
giphy: giphy_zod_ts,
"google-tag-manager": google_tag_manager_zod_ts,
googlecalendar: googlecalendar_zod_ts,
hubspot: hubspot_zod_ts,
larkcalendar: larkcalendar_zod_ts,
@@ -17,6 +17,7 @@ export const apiHandlers = {
fathom: import("./fathom/api"),
ga4: import("./ga4/api"),
giphy: import("./giphy/api"),
"google-tag-manager": import("./google-tag-manager/api"),
googlecalendar: import("./googlecalendar/api"),
googlevideo: import("./googlevideo/api"),
hubspot: import("./hubspot/api"),
@@ -39,6 +40,7 @@ export const apiHandlers = {
signal: import("./signal/api"),
sirius_video: import("./sirius_video/api"),
stripepayment: import("./stripepayment/api"),
sylapsvideo: import("./sylapsvideo/api"),
tandemvideo: import("./tandemvideo/api"),
telegram: import("./telegram/api"),
basic: import("./templates/basic/api"),
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1,38 @@
import { useState } from "react";
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
import AppCard from "@calcom/app-store/_components/AppCard";
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
import { TextField } from "@calcom/ui";
import type { appDataSchema } from "../zod";
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
const trackingId = getAppData("trackingId");
const [enabled, setEnabled] = useState(getAppData("enabled"));
return (
<AppCard
setAppData={setAppData}
app={app}
switchOnClick={(e) => {
if (!e) {
setEnabled(false);
} else {
setEnabled(true);
}
}}
switchChecked={enabled}>
<TextField
name="Tracking ID"
value={trackingId}
onChange={(e) => {
setAppData("trackingId", e.target.value);
}}
/>
</AppCard>
);
};
export default EventTypeAppCard;
@@ -0,0 +1,28 @@
{
"name": "Google Tag Manager",
"slug": "google-tag-manager",
"type": "google-tag-manager_analytics",
"imageSrc": "icon.svg",
"logo": "icon.svg",
"url": "https://tagmanager.google.com",
"variant": "analytics",
"categories": [
"analytics"
],
"publisher": "Black Lemon",
"email": "support@blacklemon.dk",
"description": "App to install Google Tag Manager",
"extendsFeature": "EventType",
"appData": {
"tag": {
"scripts": [
{
"content":"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-{TRACKING_ID}');"
}
]
}
},
"isTemplate": false,
"__createdUsingCli": true,
"__template": "booking-pages-tag"
}
@@ -0,0 +1 @@
export * as api from "./api";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/google-tag-manager",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "App to install Google Tag Manager"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

@@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.87 122.89"><defs><style>.cls-1{fill:#8ab4f8;}.cls-2{fill:#4285f4;}.cls-3{fill:#246fdb;}</style></defs><title>google-tag-manager</title><polygon class="cls-1" points="72.13 118.22 50.8 96.86 96.63 50.56 118.35 72.28 72.13 118.22 72.13 118.22"/><path class="cls-2" d="M72.29,26.23,50.56,4.5,4.5,50.56a15.35,15.35,0,0,0,0,21.71h0l46.06,46.06,21.31-21.4L37.07,61.45,72.29,26.23Z"/><path class="cls-1" d="M118.35,50.56,72.29,4.5A15.36,15.36,0,0,0,50.56,26.22L96.65,72.28a15.36,15.36,0,0,0,21.72-21.72Z"/><path class="cls-3" d="M76.48,107.66a15.23,15.23,0,1,0-15.22,15.23,15.22,15.22,0,0,0,15.22-15.23Z"/></svg>

After

Width:  |  Height:  |  Size: 695 B

@@ -0,0 +1,11 @@
import { z } from "zod";
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
export const appDataSchema = eventTypeAppCardZod.merge(
z.object({
trackingId: z.string(),
})
);
export const appKeysSchema = z.object({});
+2
View File
@@ -20,6 +20,7 @@ import * as plausible from "./plausible";
import * as salesforce from "./salesforce";
import * as sendgrid from "./sendgrid";
import * as stripepayment from "./stripepayment";
import * as sylapsvideo from "./sylapsvideo";
import * as tandemvideo from "./tandemvideo";
import * as vital from "./vital";
import * as wipemycalother from "./wipemycalother";
@@ -38,6 +39,7 @@ const appStore = {
hubspot,
huddle01video,
jitsivideo,
sylapsvideo,
larkcalendar,
office365calendar,
office365video,
@@ -1,3 +1,4 @@
import type { ChangeEvent } from "react";
import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-builder";
// Figure out why routing-forms/env.d.ts doesn't work
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -94,7 +95,18 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
if (!props) {
return <div />;
}
return <EmailField type="email" {...props} />;
return (
<EmailField
onChange={(e: ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
props.setValue(val);
}}
containerClassName="w-full"
className="dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500"
{...props}
/>
);
},
},
};
@@ -0,0 +1,6 @@
---
items:
- /api/app-store/sylaps/sylaps1.png
---
Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.
+45
View File
@@ -0,0 +1,45 @@
import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
/**
* This is an example endpoint for an app, these will run under `/api/integrations/[...args]`
* @param req
* @param res
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!req.session?.user?.id) {
return res.status(401).json({ message: "You must be logged in to do this" });
}
const appType = "sylaps_video";
try {
const alreadyInstalled = await prisma.credential.findFirst({
where: {
type: appType,
userId: req.session.user.id,
},
});
if (alreadyInstalled) {
throw new Error("Already installed");
}
const installation = await prisma.credential.create({
data: {
type: appType,
key: {},
userId: req.session.user.id,
appId: "sylaps",
},
});
if (!installation) {
throw new Error("Unable to create user credential for sylaps");
}
} catch (error: unknown) {
if (error instanceof Error) {
return res.status(500).json({ message: error.message });
}
return res.status(500);
}
return res.status(200).json({ url: getInstalledAppPath({ variant: "conferencing", slug: "sylaps" }) });
}
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1 @@
.gitkeep
@@ -0,0 +1,23 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Sylaps",
"title": "Sylaps",
"slug": "sylaps",
"type": "sylaps_video",
"imageSrc": "/api/app-store/sylaps/icon.svg",
"logo": "/api/app-store/sylaps/icon.svg",
"url": "https://cal.com/apps/sylaps",
"variant": "conferencing",
"categories": ["video"],
"publisher": "Sylaps Inc",
"email": "support@sylaps.com",
"description": "Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.",
"__createdUsingCli": true,
"appData": {
"location": {
"linkType": "dynamic",
"type": "integrations:sylaps_video",
"label": "Sylaps"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * as lib from "./lib";
export * as api from "./api";
@@ -0,0 +1,34 @@
import { v4 as uuidv4 } from "uuid";
import type { PartialReference } from "@calcom/types/EventManager";
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
const SylapsApiAdapter = (): VideoApiAdapter => {
return {
getAvailability: () => {
return Promise.resolve([]);
},
createMeeting: async (): Promise<VideoCallData> => {
const meetingID = uuidv4();
return Promise.resolve({
type: "sylaps_video",
id: meetingID,
password: "",
url: "https://sylaps.com/r/" + meetingID,
});
},
deleteMeeting: async (): Promise<void> => {
Promise.resolve();
},
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
return Promise.resolve({
type: "sylaps_video",
id: bookingRef.meetingId as string,
password: bookingRef.meetingPassword as string,
url: bookingRef.meetingUrl as string,
});
},
};
};
export default SylapsApiAdapter;
@@ -0,0 +1 @@
export { default as VideoApiAdapter } from "./VideoApiAdapter";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/sylapsvideo",
"version": "0.0.0",
"main": "./index.ts",
"description": "Free Audio and Video Conferencing, Online Collaboration, Screen Sharing on web browser, mobile and desktop.",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
}
}
@@ -0,0 +1 @@
<svg viewBox="0 0 318 318" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><use xlink:href="#a" x="255" width="318" height="318" transform="matrix(1.08202 0 0 1.08518 -289.504 -13.007)"/><path d="M126.988 77.707c6.621-7.672 13.161-15.177 19.543-22.814.431-.516.036-2.425-.599-3.021-6.041-5.662-11.83-11.695-18.427-16.64C109.3 21.586 89.027 17.757 67.314 25.074 36.697 35.392 18.186 66.19 22.801 98.267c4.15 28.837 24.33 51.186 52.908 56.744 28.895 5.62 54.399-2.534 74.886-23.504 11.478-11.749 21.051-25.341 31.726-37.896 7.402-8.705 14.589-17.75 23.007-25.393 10.772-9.779 23.805-13.01 37.68-6.757 14.16 6.382 20.803 21.031 17.142 36.146-3.611 14.908-16.076 24.446-31.371 23.875-7.912-.296-14.583-3.867-20.539-8.703-5.03-4.084-9.701-8.611-14.855-13.233l-19.731 24.625c9.626 10.85 19.982 20.108 32.778 26.305 12.635 6.119 25.924 7.785 39.613 4.901 34.945-7.361 57.368-39.123 52.927-74.668-3.626-29.021-24.928-52.216-53.51-57.637-25.757-4.886-48.429 2.398-68.468 18.706-11.754 9.566-20.742 21.653-29.963 33.515-7.215 9.283-14.273 18.705-21.855 27.681-6.101 7.223-13.527 12.849-22.954 15.37-19.787 5.29-38.579-5.736-41.277-25.005-3.016-21.536 13.96-40.408 36.739-35.904 7.873 1.557 14.12 6.308 20.005 11.469 3.141 2.753 6.078 5.739 9.299 8.803Z" style="fill:#fff;fill-rule:nonzero" transform="matrix(.81681 0 0 .8192 28.171 86.549)"/><defs><image id="a" width="318" height="318" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAT4AAAE+CAYAAAAUOHwwAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAELUlEQVR4nO3UQRGAQBDAsIXnScU0UsAFN0MTBX31mJlZ1/0MQMS5OwDga8YH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8QI7xATnGB+QYH5BjfECO8QE5xgfkGB+QY3xAjvEBOcYH5BgfkGN8AAB/9wLC9QTMTO4oPgAAAABJRU5ErkJggg=="/></defs></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@@ -1,6 +1,7 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getHumanReadableLocationValue } from "@calcom/core/location";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { defaultHandler, defaultResponder, getTranslation } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
@@ -31,6 +32,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
title: true,
description: true,
customInputs: true,
responses: true,
startTime: true,
endTime: true,
location: true,
@@ -53,6 +55,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
price: true,
currency: true,
length: true,
bookingFields: true,
},
},
attendees: {
@@ -68,7 +71,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
const t = await getTranslation(bookings[0].user?.locale ?? "en", "common");
const updatedBookings = bookings.map((booking) => {
return { ...booking, location: getHumanReadableLocationValue(booking.location || "", t) };
return {
...booking,
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
location: getHumanReadableLocationValue(booking.location || "", t),
};
});
res.status(201).json(updatedBookings);
+1
View File
@@ -30,6 +30,7 @@ const config = {
"no",
"uk",
"da",
"tr",
],
},
reloadOnPrerender: process.env.NODE_ENV !== "production",
@@ -2,23 +2,45 @@ import type z from "zod";
import { SystemField } from "@calcom/features/bookings/lib/getBookingFields";
import type { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import { getBookingWithResponses } from "@calcom/lib/getBooking";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { EventType, Prisma } from ".prisma/client";
export const getCalEventResponses = ({
bookingFields,
booking,
responses,
}: {
// If the eventType has been deleted and a booking is Accepted later on, then bookingFields will be null and we can't know the label of fields. So, we should store the label as well in the DB
// Also, it is no longer straightforward to identify if a field is system field or not
bookingFields: z.infer<typeof eventTypeBookingFields> | null;
responses: z.infer<typeof bookingResponsesDbSchema>;
bookingFields: z.infer<typeof eventTypeBookingFields> | EventType["bookingFields"] | null;
booking?: Prisma.BookingGetPayload<{
select: {
description: true;
customInputs: true;
attendees: {
select: {
email: true;
name: true;
};
};
location: true;
responses: true;
};
}>;
responses?: z.infer<typeof bookingResponsesDbSchema>;
}) => {
const calEventUserFieldsResponses = {} as NonNullable<CalendarEvent["userFieldsResponses"]>;
const calEventResponses = {} as NonNullable<CalendarEvent["responses"]>;
const parsedBookingFields = bookingFields ? eventTypeBookingFields.parse(bookingFields) : null;
const backwardCompatibleResponses =
responses ?? (booking ? getBookingWithResponses(booking).responses : null);
if (!backwardCompatibleResponses) throw new Error("Couldn't get responses");
if (bookingFields) {
bookingFields.forEach((field) => {
if (parsedBookingFields) {
parsedBookingFields.forEach((field) => {
const label = field.label || field.defaultLabel;
if (!label) {
throw new Error('Missing label for booking field "' + field.name + '"');
@@ -26,17 +48,17 @@ export const getCalEventResponses = ({
if (field.editable === "user" || field.editable === "user-readonly") {
calEventUserFieldsResponses[field.name] = {
label,
value: responses[field.name],
value: backwardCompatibleResponses[field.name],
};
}
calEventResponses[field.name] = {
label,
value: responses[field.name],
value: backwardCompatibleResponses[field.name],
};
});
} else {
// Alternative way to generate for a booking of whose eventType has been deleted
for (const [name, value] of Object.entries(responses)) {
for (const [name, value] of Object.entries(backwardCompatibleResponses)) {
const isSystemField = SystemField.safeParse(name);
// Use name for Label because we don't have access to the label. This will not be needed once we start storing the label along with the response
@@ -55,5 +77,5 @@ export const getCalEventResponses = ({
};
}
}
return { calEventUserFieldsResponses, calEventResponses };
return { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses };
};
@@ -10,6 +10,7 @@ import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler"
import { deleteMeeting, updateMeeting } from "@calcom/core/videoClient";
import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendCancelledSeatEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
@@ -67,6 +68,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
currency: true,
length: true,
seatsPerTimeSlot: true,
bookingFields: true,
seatsShowAttendees: true,
hosts: {
select: {
@@ -91,6 +93,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
workflowReminders: true,
scheduledJobs: true,
seatsReferences: true,
responses: true,
},
});
}
@@ -173,6 +176,10 @@ async function handler(req: CustomRequest) {
type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title,
description: bookingToDelete?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
...getCalEventResponses({
bookingFields: bookingToDelete.eventType?.bookingFields ?? null,
booking: bookingToDelete,
}),
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
organizer: {
@@ -509,6 +516,10 @@ async function handler(req: CustomRequest) {
title: bookingToDelete.title,
description: bookingToDelete.description ?? "",
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
...getCalEventResponses({
booking: bookingToDelete,
bookingFields: bookingToDelete.eventType?.bookingFields ?? null,
}),
startTime: bookingToDelete.startTime.toISOString(),
endTime: bookingToDelete.endTime.toISOString(),
organizer: {
@@ -404,10 +404,11 @@ function getBookingData({
const reqBody = bookingDataSchema.parse(req.body);
if ("responses" in reqBody) {
const responses = reqBody.responses;
const { calEventUserFieldsResponses, calEventResponses } = getCalEventResponses({
bookingFields: eventType.bookingFields,
responses,
});
const { userFieldsResponses: calEventUserFieldsResponses, responses: calEventResponses } =
getCalEventResponses({
bookingFields: eventType.bookingFields,
responses,
});
return {
...reqBody,
name: responses.name,
@@ -742,6 +743,7 @@ async function handler(
const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
const bookingLocation = getLocationValueForDB(locationBodyString, eventType.locations);
const customInputs = getCustomInputsResponses(reqBody, eventType.customInputs);
const teamMemberPromises =
users.length > 1
@@ -7,6 +7,7 @@ import type Stripe from "stripe";
import stripe from "@calcom/app-store/stripepayment/lib/server";
import EventManager from "@calcom/core/EventManager";
import { sendScheduledEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { IS_PRODUCTION } from "@calcom/lib/constants";
@@ -65,6 +66,7 @@ async function handlePaymentSuccess(event: Stripe.Event) {
paid: true,
destinationCalendar: true,
status: true,
responses: true,
user: {
select: {
id: true,
@@ -114,6 +116,10 @@ async function handlePaymentSuccess(event: Stripe.Event) {
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
booking: booking,
bookingFields: booking.eventType?.bookingFields || null,
}),
organizer: {
email: user.email,
name: user.name!,
@@ -6,34 +6,45 @@ import { trpc } from "@calcom/trpc";
import { useFilterContext } from "../context/provider";
import { valueFormatter } from "../lib/valueFormatter";
import { CardInsights } from "./Card";
import { LoadingInsight } from "./LoadingInsights";
export const AverageEventDurationChart = () => {
const { t } = useLocale();
const { filter } = useFilterContext();
const { dateRange, selectedUserId } = filter;
const { dateRange, selectedMemberUserId } = filter;
const [startDate, endDate] = dateRange;
const { selectedTeamId: teamId } = filter;
const { selectedTeamId: teamId, selectedUserId } = filter;
const { data, isSuccess } = trpc.viewer.insights.averageEventDuration.useQuery({
const { data, isSuccess, isLoading } = trpc.viewer.insights.averageEventDuration.useQuery({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
teamId,
teamId: teamId ?? undefined,
memberUserId: selectedMemberUserId ?? undefined,
userId: selectedUserId ?? undefined,
});
if (!isSuccess || data?.length == 0 || !startDate || !endDate || !teamId) return null;
if (isLoading) return <LoadingInsight />;
if (!isSuccess || !startDate || !endDate || (!teamId && !selectedUserId)) return null;
const isNoData = (data && data.length === 0) || data.every((item) => item["Average"] === 0);
return (
<CardInsights>
<Title>{t("average_event_duration")}</Title>
<LineChart
className="mt-4 h-80"
data={data}
index="Date"
categories={["Average"]}
colors={["blue"]}
valueFormatter={valueFormatter}
/>
{isNoData && (
<div className="flex h-60 text-center">
<p className="m-auto text-sm font-light">{t("insights_no_data_found_for_filter")}</p>
</div>
)}
{data && data.length > 0 && !isNoData && (
<LineChart
className="mt-4 h-80"
data={data}
index="Date"
categories={["Average"]}
colors={["blue"]}
valueFormatter={valueFormatter}
/>
)}
</CardInsights>
);
};
@@ -2,23 +2,26 @@ import { Grid } from "@tremor/react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc";
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
import { useFilterContext } from "../context/provider";
import { CardInsights } from "./Card";
import { KPICard } from "./KPICard";
export const BookingKPICards = () => {
const { t } = useLocale();
const { filter } = useFilterContext();
const { dateRange, selectedEventTypeId, selectedUserId } = filter;
const { dateRange, selectedEventTypeId, selectedUserId, selectedMemberUserId } = filter;
const [startDate, endDate] = dateRange;
const { selectedTeamId: teamId } = filter;
const { data, isSuccess } = trpc.viewer.insights.eventsByStatus.useQuery({
const { data, isSuccess, isLoading } = trpc.viewer.insights.eventsByStatus.useQuery({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
teamId,
eventTypeId: selectedEventTypeId ?? undefined,
memberUserId: selectedMemberUserId ?? undefined,
userId: selectedUserId ?? undefined,
});
@@ -44,18 +47,41 @@ export const BookingKPICards = () => {
},
];
if (!isSuccess || !startDate || !endDate || !teamId || data?.empty) return null;
if (isLoading) {
return <LoadingKPICards categories={categories} />;
}
if (!isSuccess || !startDate || !endDate || (!teamId && !selectedUserId)) return null;
return (
<>
<Grid numColsSm={2} numColsLg={4} className="gap-x-6 gap-y-6">
{categories.map((item) => (
<KPICard
key={item.title}
title={item.title}
value={data[item.index].count}
previousMetricData={data[item.index]}
previousDateRange={data.previousRange}
/>
))}
</Grid>
</>
);
};
const LoadingKPICards = (props: { categories: { title: string; index: string }[] }) => {
const { categories } = props;
return (
<Grid numColsSm={2} numColsLg={4} className="gap-x-6 gap-y-6">
{categories.map((item) => (
<KPICard
key={item.title}
title={item.title}
value={data[item.index].count}
previousMetricData={data[item.index]}
previousDateRange={data.previousRange}
/>
<CardInsights key={item.title}>
<SkeletonContainer className="flex w-full flex-col">
<SkeletonText className="mt-2 h-4 w-32" />
<SkeletonText className="mt-2 h-6 w-16" />
<SkeletonText className="mt-4 h-6 w-44" />
</SkeletonContainer>
</CardInsights>
))}
</Grid>
);
@@ -6,23 +6,37 @@ import { trpc } from "@calcom/trpc";
import { useFilterContext } from "../context/provider";
import { valueFormatter } from "../lib/valueFormatter";
import { CardInsights } from "./Card";
import { LoadingInsight } from "./LoadingInsights";
export const BookingStatusLineChart = () => {
const { t } = useLocale();
const { filter } = useFilterContext();
const { selectedTeamId, selectedTimeView = "week", dateRange, selectedEventTypeId } = filter;
const {
selectedTeamId,
selectedUserId,
selectedTimeView = "week",
dateRange,
selectedEventTypeId,
} = filter;
const [startDate, endDate] = dateRange;
if (!startDate || !endDate) return null;
const { data: eventsTimeLine, isSuccess } = trpc.viewer.insights.eventsTimeline.useQuery({
const {
data: eventsTimeLine,
isSuccess,
isLoading,
} = trpc.viewer.insights.eventsTimeline.useQuery({
timeView: selectedTimeView,
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
teamId: selectedTeamId || -1,
teamId: selectedTeamId ?? undefined,
eventTypeId: selectedEventTypeId ?? undefined,
userId: selectedUserId ?? undefined,
});
if (isLoading) return <LoadingInsight />;
if (!isSuccess) return null;
return (
@@ -1,5 +1,7 @@
import { Card } from "@tremor/react";
import { classNames } from "@calcom/lib";
interface ICardProps {
children: React.ReactNode;
className?: string;
@@ -9,7 +11,7 @@ export const CardInsights = (props: ICardProps) => {
const { children, className = "", ...rest } = props;
return (
<Card className={`shadow-none ring-gray-300 ${className}`} {...rest}>
<Card className={classNames("shadow-none ring-gray-200", className)} {...rest}>
{children}
</Card>
);
@@ -5,6 +5,7 @@ import { trpc } from "@calcom/trpc";
import { useFilterContext } from "../context/provider";
import { CardInsights } from "./Card";
import { LoadingInsight } from "./LoadingInsights";
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
export const LeastBookedTeamMembersTable = () => {
@@ -13,13 +14,15 @@ export const LeastBookedTeamMembersTable = () => {
const { dateRange, selectedEventTypeId, selectedTeamId: teamId } = filter;
const [startDate, endDate] = dateRange;
const { data, isSuccess } = trpc.viewer.insights.membersWithLeastBookings.useQuery({
const { data, isSuccess, isLoading } = trpc.viewer.insights.membersWithLeastBookings.useQuery({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
teamId,
eventTypeId: selectedEventTypeId ?? undefined,
});
if (isLoading) return <LoadingInsight />;
if (!isSuccess || !startDate || !endDate || !teamId) return null;
return (
@@ -0,0 +1,26 @@
import { classNames } from "@calcom/lib";
import { SkeletonText } from "@calcom/ui";
import { CardInsights } from "./Card";
export const LoadingInsight = () => {
return (
<CardInsights>
<SkeletonText className="w-32" />
<div className="m-auto flex h-80 flex-col items-center justify-center">
<svg
className={classNames("mx-4 h-8 w-8 animate-spin", "text-black dark:text-white")}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
</div>
</CardInsights>
);
};
@@ -5,6 +5,7 @@ import { trpc } from "@calcom/trpc";
import { useFilterContext } from "../context/provider";
import { CardInsights } from "./Card";
import { LoadingInsight } from "./LoadingInsights";
import { TotalBookingUsersTable } from "./TotalBookingUsersTable";
export const MostBookedTeamMembersTable = () => {
@@ -14,13 +15,15 @@ export const MostBookedTeamMembersTable = () => {
const [startDate, endDate] = dateRange;
const { selectedTeamId: teamId } = filter;
const { data, isSuccess } = trpc.viewer.insights.membersWithMostBookings.useQuery({
const { data, isSuccess, isLoading } = trpc.viewer.insights.membersWithMostBookings.useQuery({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
teamId,
eventTypeId: selectedEventTypeId ?? undefined,
});
if (isLoading) return <LoadingInsight />;
if (!isSuccess || !startDate || !endDate || !teamId) return null;
return (
@@ -5,22 +5,26 @@ import { trpc } from "@calcom/trpc";
import { useFilterContext } from "../context/provider";
import { CardInsights } from "./Card";
import { LoadingInsight } from "./LoadingInsights";
export const PopularEventsTable = () => {
const { t } = useLocale();
const { filter } = useFilterContext();
const { dateRange, selectedUserId } = filter;
const { dateRange, selectedMemberUserId, selectedUserId } = filter;
const [startDate, endDate] = dateRange;
const { selectedTeamId: teamId } = filter;
const { data, isSuccess } = trpc.viewer.insights.popularEventTypes.useQuery({
const { data, isSuccess, isLoading } = trpc.viewer.insights.popularEventTypes.useQuery({
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
teamId,
teamId: teamId ?? undefined,
userId: selectedUserId ?? undefined,
memberUserId: selectedMemberUserId ?? undefined,
});
if (!isSuccess || !startDate || !endDate || !teamId || data?.length === 0) return null;
if (isLoading) return <LoadingInsight />;
if (!isSuccess || !startDate || !endDate || (!teamId && !selectedUserId)) return null;
return (
<CardInsights>
@@ -30,7 +34,7 @@ export const PopularEventsTable = () => {
{data.map((item) => (
<TableRow key={item.eventTypeId}>
<TableCell>{item.eventTypeName}</TableCell>
<TableCell>
<TableCell className="text-right">
<Text>
<strong>{item.count}</strong>
</Text>
@@ -39,6 +43,11 @@ export const PopularEventsTable = () => {
))}
</TableBody>
</Table>
{data.length === 0 && (
<div className="flex h-60 text-center">
<p className="m-auto text-sm font-light">{t("insights_no_data_found_for_filter")}</p>
</div>
)}
</CardInsights>
);
};
@@ -30,7 +30,7 @@ export const TotalBookingUsersTable = ({
<strong>{item.user.name}</strong>
</p>
</TableCell>
<TableCell>
<TableCell className="text-right">
<Text>
<strong>{item.count}</strong>
</Text>
@@ -10,17 +10,24 @@ import { FilterProvider } from "./provider";
export function FiltersProvider({ children }: { children: React.ReactNode }) {
// useRouter to get initial values from query params
const router = useRouter();
const { startTime, endTime, teamId, userId, eventTypeId, filter } = router.query;
const { startTime, endTime, teamId, userId, eventTypeId, filter, memberUserId } = router.query;
const querySchema = z.object({
startTime: z.string().optional(),
endTime: z.string().optional(),
teamId: z.coerce.number().optional(),
userId: z.coerce.number().optional(),
memberUserId: z.coerce.number().optional(),
eventTypeId: z.coerce.number().optional(),
filter: z.enum(["event-type", "user"]).optional(),
});
let startTimeParsed, endTimeParsed, teamIdParsed, userIdParsed, eventTypeIdParsed, filterParsed;
let startTimeParsed,
endTimeParsed,
teamIdParsed: number | undefined,
userIdParsed,
eventTypeIdParsed,
filterParsed,
memberUserIdParsed;
const safe = querySchema.safeParse({
startTime,
@@ -29,6 +36,7 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
userId,
eventTypeId,
filter,
memberUserId,
});
if (!safe.success) {
@@ -40,6 +48,7 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
userIdParsed = safe.data.userId;
eventTypeIdParsed = safe.data.eventTypeId;
filterParsed = safe.data.filter;
memberUserIdParsed = safe.data.memberUserId;
}
// TODO: Sync insight filters with URL parameters
@@ -48,6 +57,9 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
const [selectedUserId, setSelectedUserId] = useState<FilterContextType["filter"]["selectedUserId"]>(
userIdParsed || null
);
const [selectedMemberUserId, setSelectedMemberUserId] = useState<
FilterContextType["filter"]["selectedMemberUserId"]
>(memberUserIdParsed || null);
const [selectedTeamId, setSelectedTeamId] = useState<FilterContextType["filter"]["selectedTeamId"]>(
teamIdParsed || null
);
@@ -64,21 +76,24 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
endTimeParsed ? dayjs(endTimeParsed) : dayjs(),
"t",
]);
return (
<FilterProvider
value={{
filter: {
dateRange,
selectedTimeView,
selectedUserId,
selectedMemberUserId,
selectedTeamId,
selectedUserId,
selectedTeamName,
selectedEventTypeId,
selectedFilter,
},
setSelectedFilter: (filter) => {
setSelectedFilter(filter);
const userId = filter?.[0] === "user" ? selectedUserId : undefined;
const userId =
filter?.[0] === "user" ? selectedMemberUserId : selectedUserId ? selectedUserId : undefined;
const eventTypeId = filter?.[0] === "event-type" ? selectedEventTypeId : undefined;
router.push({
query: {
@@ -100,24 +115,38 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
});
},
setSelectedTimeView: (selectedTimeView) => setSelectedTimeView(selectedTimeView),
setSelectedUserId: (selectedUserId) => {
setSelectedUserId(selectedUserId);
setSelectedMemberUserId: (selectedMemberUserId) => {
setSelectedMemberUserId(selectedMemberUserId);
router.push({
query: {
...router.query,
userId: selectedUserId,
memberUserId: selectedMemberUserId,
},
});
},
setSelectedTeamId: (selectedTeamId) => {
setSelectedTeamId(selectedTeamId);
router.push({
setSelectedUserId(null);
const { teamId, eventTypeId, memberUserId, ...rest } = router.query;
router.replace({
query: {
...router.query,
...rest,
teamId: selectedTeamId,
},
});
},
setSelectedUserId: (selectedUserId) => {
setSelectedUserId(selectedUserId);
setSelectedTeamId(null);
setSelectedTeamName(null);
const { teamId, eventTypeId, memberUserId, ...rest } = router.query;
router.replace({
query: {
...rest,
userId: selectedUserId,
},
});
},
setSelectedTeamName: (selectedTeamName) => setSelectedTeamName(selectedTeamName),
setSelectedEventTypeId: (selectedEventTypeId) => {
setSelectedEventTypeId(selectedEventTypeId);
@@ -128,6 +157,30 @@ export function FiltersProvider({ children }: { children: React.ReactNode }) {
},
});
},
clearFilters: () => {
setSelectedTeamName(null);
setSelectedEventTypeId(null);
setSelectedMemberUserId(null);
setSelectedFilter(null);
const { teamId, userId, ...rest } = router.query;
const query: { teamId?: number; userId?: number } = {};
const parsedTeamId = Number(Array.isArray(teamId) ? teamId[0] : teamId);
const parsedUserId = Number(Array.isArray(userId) ? userId[0] : userId);
if ((teamId && !userId) || (userId && teamId)) {
query.teamId = parsedTeamId;
setSelectedTeamId(parsedTeamId);
setSelectedUserId(null);
} else if (userId && !teamId) {
query.userId = parsedUserId;
setSelectedUserId(parsedUserId);
setSelectedTeamId(null);
}
router.push({
query,
});
},
}}>
{children}
</FilterProvider>
@@ -10,6 +10,7 @@ export type FilterContextType = {
selectedTeamId: number | null;
selectedTeamName: string | null;
selectedUserId: number | null;
selectedMemberUserId: number | null;
selectedEventTypeId: number | null;
};
setDateRange: ([start, end, range]: [Dayjs, Dayjs, null | string]) => void;
@@ -17,8 +18,10 @@ export type FilterContextType = {
setSelectedTeamId: (teamId: number | null) => void;
setSelectedTeamName: (teamName: string | null) => void;
setSelectedUserId: (userId: number | null) => void;
setSelectedMemberUserId: (userId: number | null) => void;
setSelectedEventTypeId: (eventTypeId: number | null) => void;
setSelectedTimeView: (timeView: "year" | "week" | "month") => void;
clearFilters: () => void;
};
export const FilterContext = React.createContext<FilterContextType | null>(null);
@@ -1,7 +1,28 @@
.custom-date > .tremor-DateRangePicker-root > .tremor-DateRangePicker-button {
box-shadow: none;
max-width: 288px;
}
.tremor-DateRangePicker-calendarButton, .tremor-DateRangePicker-dropdownButton {
@apply dark:bg-darkgray-100 dark:border-darkgray-300 border-gray-300 bg-white text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-0 focus-within:ring-gray-800 hover:border-gray-400 dark:focus-within:ring-darkgray-900
}
.tremor-Card-root{
@apply p-5
}
.tremor-TableCell-root{
@apply pl-0
}
.recharts-responsive-container{
@apply -mx-4
}
.tremor-Card-root > p {
@apply text-base font-semibold text-black mb-2
}
.tremor-Legend-legendItem{
@apply ml-2
}
@@ -15,17 +15,18 @@ const mapEventTypeToOption = (eventType: EventType): Option => ({
label: eventType.title,
});
export const EventTypeListInTeam = () => {
export const EventTypeList = () => {
const { t } = useLocale();
const { filter, setSelectedEventTypeId } = useFilterContext();
const { selectedTeamId, selectedEventTypeId } = filter;
const { selectedTeamId, selectedEventTypeId, selectedUserId } = filter;
const { selectedFilter } = filter;
const { data, isSuccess } = trpc.viewer.insights.eventTypeList.useQuery({
teamId: selectedTeamId,
teamId: selectedTeamId ?? undefined,
userId: selectedUserId ?? undefined,
});
if (!selectedFilter?.includes("event-type")) return null;
if (!selectedTeamId) return null;
if (!selectedTeamId && !selectedUserId) return null;
const filterOptions =
data?.map(mapEventTypeToOption) ?? ([{ label: "No event types found", value: "" }] as Option[]);
@@ -46,6 +47,7 @@ export const EventTypeListInTeam = () => {
}
}}
defaultValue={eventTypeValue}
value={eventTypeValue}
className="w-52 min-w-[180px]"
placeholder={
<div className="flex flex-row">
@@ -8,10 +8,10 @@ type Option = { value: "event-type" | "user"; label: string };
export const FilterType = () => {
const { t } = useLocale();
const { setSelectedFilter, setSelectedUserId, setSelectedEventTypeId, filter } = useFilterContext();
const { selectedFilter } = filter;
const { setSelectedFilter, filter } = useFilterContext();
const { selectedFilter, selectedUserId } = filter;
const filterOptions: Option[] = [
let filterOptions: Option[] = [
{
label: t("event_type"),
value: "event-type",
@@ -22,9 +22,14 @@ export const FilterType = () => {
},
];
if (selectedUserId) {
// remove user option from filterOptions
filterOptions = filterOptions.filter((option) => option.value !== "user");
}
const filterValue = selectedFilter
? filterOptions.find((option) => option.value === selectedFilter[0])
: undefined;
: null;
return (
<Select<Option>
@@ -7,30 +7,42 @@ import { Select } from "@calcom/ui";
import { useFilterContext } from "../context/provider";
type Team = RouterOutputs["viewer"]["insights"]["teamListForUser"][number];
type Option = { value: number; label: string };
type Option = { value: number; label: string | null; userId?: number };
const mapTeamToOption = (team: Team): Option => ({
value: team.id,
label: team.name ?? "",
userId: team.userId,
});
export const TeamList = () => {
const { filter, setSelectedTeamId, setSelectedTeamName } = useFilterContext();
const { selectedTeamId } = filter;
export const TeamAndSelfList = () => {
const { filter, setSelectedTeamId, setSelectedTeamName, setSelectedUserId } = useFilterContext();
const { selectedTeamId, selectedUserId } = filter;
const { data, isSuccess } = trpc.viewer.insights.teamListForUser.useQuery();
useEffect(() => {
if (data && data?.length > 0) {
setSelectedTeamId(data[0].id);
setSelectedTeamName(data[0].name);
// We have a team?
if (data[0].id) {
setSelectedTeamId(data[0].id);
setSelectedTeamName(data[0].name);
} else if (data[0].userId) {
// default to user
setSelectedUserId(data[0].userId);
}
}
}, [data]);
const UserListOptions = data?.map(mapTeamToOption) || ([{ label: "Empty", value: -1 }] as Option[]);
const selectedTeam = data?.find((item) => item.id === selectedTeamId);
const selectedTeam = data?.find((item) => {
if (!!selectedUserId && !selectedTeamId) {
return item.userId === selectedUserId;
}
return item.id === selectedTeamId;
});
const teamValue = selectedTeam ? mapTeamToOption(selectedTeam) : null;
if (!isSuccess || !selectedTeamId || data?.length === 0) return null;
if (!isSuccess || data?.length === 0) return null;
return (
<>
@@ -39,10 +51,12 @@ export const TeamList = () => {
isMulti={false}
value={teamValue}
defaultValue={selectedTeamId ? { value: data[0].id, label: data[0].name } : null}
className="h-[38px] w-[90vw] capitalize md:min-w-[100px] md:max-w-[100px]"
className="h-[38px] w-[90vw] min-w-[160px] max-w-[100px]"
options={UserListOptions}
onChange={(input) => {
if (input) {
if (!!input?.userId) {
setSelectedUserId(input.userId);
} else if (input && input.value) {
setSelectedTeamId(input.value);
setSelectedTeamName(input.label);
}
@@ -15,8 +15,8 @@ const mapUserToOption = (user: User): Option => ({
export const UserListInTeam = () => {
const { t } = useLocale();
const { filter, setSelectedUserId } = useFilterContext();
const { selectedFilter, selectedTeamId, selectedUserId } = filter;
const { filter, setSelectedMemberUserId } = useFilterContext();
const { selectedFilter, selectedTeamId, selectedMemberUserId } = filter;
const { data, isSuccess } = trpc.viewer.insights.userList.useQuery({
teamId: selectedTeamId,
});
@@ -25,20 +25,21 @@ export const UserListInTeam = () => {
if (!selectedTeamId) return null;
const userListOptions = data?.map(mapUserToOption) ?? ([] as { value: number; label: string }[]);
const selectedUser = data?.find((item) => item.id === selectedUserId);
const userValue = selectedUser ? mapUserToOption(selectedUser) : null;
const selectedTeamUser = data?.find((item) => item.id === selectedMemberUserId);
const userValue = selectedTeamUser ? mapUserToOption(selectedTeamUser) : null;
if (!isSuccess || data?.length === 0) return null;
return (
<Select<Option>
isSearchable={false}
className="mb-0 ml-2 h-[38px] w-40 min-w-[140px] capitalize md:min-w-[150px] md:max-w-[200px]"
isSearchable={true}
className="mb-0 h-[38px] w-40 min-w-[140px] capitalize md:min-w-[150px] md:max-w-[200px]"
defaultValue={userValue}
value={userValue}
options={userListOptions}
onChange={(input) => {
if (input) {
setSelectedUserId(input.value);
setSelectedMemberUserId(input.value);
}
}}
placeholder={
+9 -12
View File
@@ -1,17 +1,16 @@
import { useFilterContext } from "@calcom/features/insights/context/provider";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Tooltip } from "@calcom/ui";
import { FiX } from "@calcom/ui/components/icon";
import { DateSelect } from "./DateSelect";
import { EventTypeListInTeam } from "./EventTypeListInTeam";
import { EventTypeList } from "./EventTypeList";
import { FilterType } from "./FilterType";
import { TeamList } from "./TeamList";
import { TeamAndSelfList } from "./TeamAndSelfList";
import { UserListInTeam } from "./UsersListInTeam";
const ClearFilters = () => {
const { t } = useLocale();
const { filter, setSelectedUserId, setSelectedFilter, setSelectedEventTypeId } = useFilterContext();
const { filter, clearFilters } = useFilterContext();
const { selectedFilter } = filter;
if (!selectedFilter || selectedFilter?.length < 1) return null;
@@ -23,14 +22,12 @@ const ClearFilters = () => {
color="secondary"
target="_blank"
rel="noreferrer"
StartIcon={FiX}
className="h-[38px]"
onClick={() => {
setSelectedFilter(null);
setSelectedUserId(null);
setSelectedEventTypeId(null);
}}
/>
clearFilters();
}}>
{t("clear_filters")}
</Button>
</Tooltip>
);
};
@@ -38,13 +35,13 @@ const ClearFilters = () => {
export const Filters = () => {
return (
<div className="mt-2 flex flex-col flex-wrap gap-2 md:flex-row lg:flex-nowrap">
<TeamList />
<TeamAndSelfList />
<FilterType />
<UserListInTeam />
<EventTypeListInTeam />
<EventTypeList />
<DateSelect />
+7 -4
View File
@@ -132,12 +132,15 @@ class EventsInsights {
return resultTimeView;
};
static getWeekTimeline(startDate: Dayjs, endDate: Dayjs) {
static getWeekTimeline(startDate: Dayjs, endDate: Dayjs): string[] {
const now = dayjs();
const endOfDay = now.endOf("day");
let pivotDate = dayjs(startDate);
const dates = [];
while (pivotDate.isBefore(endDate)) {
pivotDate = pivotDate.add(7, "day");
const dates: string[] = [];
while (pivotDate.isBefore(endDate) && pivotDate.isBefore(endOfDay)) {
const weekEndDate = pivotDate.add(7, "day").isBefore(endOfDay) ? pivotDate.add(7, "day") : endOfDay;
dates.push(pivotDate.format("YYYY-MM-DD"));
pivotDate = weekEndDate.add(1, "day");
}
return dates;
}
+460 -155
View File
@@ -10,7 +10,7 @@ import { TRPCError } from "@trpc/server";
import { EventsInsights } from "./events";
const UserBelongsToTeamInput = z.object({
teamId: z.coerce.number().optional(),
teamId: z.coerce.number().optional().nullable(),
});
const userBelongsToTeamMiddleware = isAuthed.unstable_pipe(async ({ ctx, next, rawInput }) => {
@@ -19,14 +19,22 @@ const userBelongsToTeamMiddleware = isAuthed.unstable_pipe(async ({ ctx, next, r
throw new TRPCError({ code: "BAD_REQUEST" });
}
const team = await ctx.prisma.membership.findFirst({
where: {
userId: ctx.user.id,
teamId: parse.data.teamId,
},
// If teamId is provided, check if user belongs to team
// If teamId is not provided, check if user belongs to any team
const membershipWhereConditional: Prisma.MembershipWhereInput = {
userId: ctx.user.id,
};
if (parse.data.teamId) {
membershipWhereConditional["teamId"] = parse.data.teamId;
}
const membership = await ctx.prisma.membership.findFirst({
where: membershipWhereConditional,
});
if (!team) {
if (!membership) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
@@ -74,28 +82,59 @@ export const insightsRouter = router({
startDate: z.string(),
endDate: z.string(),
eventTypeId: z.coerce.number().optional(),
memberUserId: z.coerce.number().optional(),
userId: z.coerce.number().optional(),
})
)
.query(async ({ ctx, input }) => {
const { teamId, startDate, endDate, eventTypeId, userId } = input;
const { teamId, startDate, endDate, eventTypeId, memberUserId, userId } = input;
if (!input.teamId) {
return emptyResponseEventsByStatus;
if (userId && userId !== ctx.user.id) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const whereConditional: Prisma.BookingWhereInput = {
eventType: {
teamId: teamId,
},
};
let whereConditional: Prisma.BookingWhereInput = {};
if (eventTypeId) {
whereConditional["eventTypeId"] = eventTypeId;
} else if (userId) {
}
if (memberUserId) {
whereConditional["userId"] = memberUserId;
}
if (userId) {
whereConditional["eventType"] = {
teamId: null,
};
whereConditional["userId"] = userId;
}
if (teamId) {
const usersFromTeam = await ctx.prisma.membership.findMany({
where: {
teamId: teamId,
},
select: {
userId: true,
},
});
const userIdsFromTeam = usersFromTeam.map((u) => u.userId);
whereConditional = {
...whereConditional,
OR: [
{
eventType: {
teamId: teamId,
},
},
{
userId: {
in: userIdsFromTeam,
},
},
],
};
}
// Migrate to use prisma views
const baseBookings = await EventsInsights.getBaseBookingForEventStatus({
...whereConditional,
@@ -103,10 +142,8 @@ export const insightsRouter = router({
gte: new Date(startDate),
lte: new Date(endDate),
},
eventType: {
teamId: teamId,
},
});
const startTimeEndTimeDiff = dayjs(endDate).diff(dayjs(startDate), "day");
const baseBookingIds = baseBookings.map((b) => b.id);
@@ -136,8 +173,7 @@ export const insightsRouter = router({
);
const lastPeriodTotalCancelled = await EventsInsights.getTotalCancelledEvents(lastPeriodBaseBookingIds);
return {
const result = {
empty: false,
created: {
count: baseBookings.length,
@@ -163,16 +199,26 @@ export const insightsRouter = router({
endDate: lastPeriodEndDate.format("YYYY-MM-DD"),
},
};
if (
result.created.count === 0 &&
result.completed.count === 0 &&
result.rescheduled.count === 0 &&
result.cancelled.count === 0
) {
return emptyResponseEventsByStatus;
}
return result;
}),
eventsTimeline: userBelongsToTeamProcedure
.input(
z.object({
teamId: z.coerce.number().optional(),
teamId: z.coerce.number().optional().nullable(),
startDate: z.string(),
endDate: z.string(),
eventTypeId: z.coerce.number().optional(),
userId: z.coerce.number().optional(),
memberUserId: z.coerce.number().optional(),
timeView: z.enum(["week", "month", "year"]),
userId: z.coerce.number().optional(),
})
)
.query(async ({ ctx, input }) => {
@@ -181,36 +227,75 @@ export const insightsRouter = router({
startDate: startDateString,
endDate: endDateString,
eventTypeId,
userId,
memberUserId,
timeView: inputTimeView,
userId: selfUserId,
} = input;
const startDate = dayjs(startDateString);
const endDate = dayjs(endDateString);
const user = ctx.user;
if (selfUserId && user?.id !== selfUserId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (!teamId && !selfUserId) {
return [];
}
const timeView = inputTimeView;
let whereConditional: Prisma.BookingWhereInput = {
eventType: {
teamId: teamId,
},
};
let whereConditional: Prisma.BookingWhereInput = {};
if (teamId) {
const usersFromTeam = await ctx.prisma.membership.findMany({
where: {
teamId,
},
select: {
userId: true,
},
});
const userIdsFromTeams = usersFromTeam.map((u) => u.userId);
if (userId) {
delete whereConditional.eventType;
whereConditional = {
...whereConditional,
userId,
OR: [
{
eventType: {
teamId,
},
},
{
userId: {
in: userIdsFromTeams,
},
},
],
};
}
if (eventTypeId && !!whereConditional) {
delete whereConditional.eventType;
delete whereConditional.userId;
if (memberUserId) {
whereConditional = {
...whereConditional,
userId: memberUserId,
};
}
if (eventTypeId && !!whereConditional) {
whereConditional = {
eventTypeId: eventTypeId,
};
}
if (selfUserId && !!whereConditional) {
// In this delete we are deleting the teamId filter
whereConditional["userId"] = selfUserId;
whereConditional["eventType"] = {
teamId: null,
};
}
// Get timeline data
const timeline = await EventsInsights.getTimeLine(timeView, dayjs(startDate), dayjs(endDate));
@@ -277,37 +362,73 @@ export const insightsRouter = router({
popularEventTypes: userBelongsToTeamProcedure
.input(
z.object({
userId: z.coerce.number().optional(),
memberUserId: z.coerce.number().optional(),
teamId: z.coerce.number().optional().nullable(),
startDate: z.string(),
endDate: z.string(),
userId: z.coerce.number().optional(),
})
)
.query(async ({ ctx, input }) => {
const { teamId, startDate, endDate, userId } = input;
const { teamId, startDate, endDate, memberUserId, userId } = input;
const user = ctx.user;
if (!input.teamId) {
if (userId && user?.id !== userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (!teamId && !userId) {
return [];
}
const eventTypeWhere: Prisma.EventTypeWhereInput = {
teamId: teamId,
};
const bookingWhere: Prisma.BookingWhereInput = {
eventType: eventTypeWhere,
let bookingWhere: Prisma.BookingWhereInput = {
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
},
};
if (teamId) {
const usersFromTeam = await ctx.prisma.membership.findMany({
where: {
teamId,
},
select: {
userId: true,
},
});
const userIdsFromTeams = usersFromTeam.map((u) => u.userId);
bookingWhere = {
...bookingWhere,
OR: [
{
eventType: {
teamId,
},
},
{
userId: {
in: userIdsFromTeams,
},
},
],
};
}
if (userId) {
bookingWhere.userId = userId;
// Don't take bookings from any team
bookingWhere.eventType = {
teamId: null,
};
}
const bookingsFromTeam = await ctx.prisma.booking.groupBy({
if (memberUserId) {
bookingWhere.userId = memberUserId;
}
const bookingsFromSelected = await ctx.prisma.booking.groupBy({
by: ["eventTypeId"],
where: bookingWhere,
_count: {
@@ -320,66 +441,158 @@ export const insightsRouter = router({
},
take: 10,
});
const eventTypeIds = bookingsFromTeam
const eventTypeIds = bookingsFromSelected
.filter((booking) => typeof booking.eventTypeId === "number")
.map((booking) => booking.eventTypeId);
const eventTypesFromTeam = await ctx.prisma.eventType.findMany({
where: {
teamId: teamId,
id: {
in: eventTypeIds as number[],
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {
id: {
in: eventTypeIds as number[],
},
};
const eventTypesFrom = await ctx.prisma.eventType.findMany({
select: {
id: true,
title: true,
teamId: true,
userId: true,
slug: true,
users: {
select: {
username: true,
},
},
team: {
select: {
slug: true,
},
},
},
where: eventTypeWhereConditional,
});
const eventTypeHashMap = new Map();
eventTypesFromTeam.forEach((eventType) => {
eventTypeHashMap.set(eventType.id, eventType.title);
const eventTypeHashMap: Map<
number,
Prisma.EventTypeGetPayload<{
select: {
id: true;
title: true;
teamId: true;
userId: true;
slug: true;
users: {
select: {
username: true;
};
};
team: {
select: {
slug: true;
};
};
};
}>
> = new Map();
eventTypesFrom.forEach((eventType) => {
eventTypeHashMap.set(eventType.id, eventType);
});
const result = bookingsFromTeam.map((booking) => {
const result = bookingsFromSelected.map((booking) => {
const eventTypeSelected = eventTypeHashMap.get(booking.eventTypeId ?? 0);
if (!eventTypeSelected) {
return {};
}
let eventSlug = "";
if (eventTypeSelected.userId) {
eventSlug = `${eventTypeSelected?.slug}/${eventTypeSelected?.users[0]?.username}`;
}
if (eventTypeSelected?.team && eventTypeSelected?.team?.slug) {
eventSlug = `${eventTypeSelected.team.slug}/${eventTypeSelected.slug}`;
}
return {
eventTypeId: booking.eventTypeId,
eventTypeName: eventTypeHashMap.get(booking.eventTypeId),
eventTypeName: eventSlug,
count: booking._count.id,
};
});
return result;
}),
averageEventDuration: userBelongsToTeamProcedure
.input(
z.object({
userId: z.coerce.number().optional(),
memberUserId: z.coerce.number().optional(),
teamId: z.coerce.number().optional().nullable(),
startDate: z.string(),
endDate: z.string(),
userId: z.coerce.number().optional(),
})
)
.query(async ({ ctx, input }) => {
const { teamId, startDate: startDateString, endDate: endDateString, userId } = input;
const { teamId, startDate: startDateString, endDate: endDateString, memberUserId, userId } = input;
if (!teamId) {
if (userId && ctx.user?.id !== userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (!teamId && !userId) {
return [];
}
const user = ctx.user;
const startDate = dayjs(startDateString);
const endDate = dayjs(endDateString);
const whereConditional: Prisma.BookingWhereInput = {
eventType: {
teamId: teamId,
},
let whereConditional: Prisma.BookingWhereInput = {
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
},
};
if (userId) {
delete whereConditional.eventType;
whereConditional["userId"] = userId;
}
if (teamId) {
const usersFromTeam = await ctx.prisma.membership.findMany({
where: {
teamId,
},
select: {
userId: true,
},
});
const userIdsFromTeams = usersFromTeam.map((u) => u.userId);
whereConditional = {
...whereConditional,
OR: [
{
eventType: {
teamId,
},
},
{
userId: {
in: userIdsFromTeams,
},
},
],
};
}
if (memberUserId) {
whereConditional = {
userId: memberUserId,
eventType: {
teamId,
},
};
}
const timeView = EventsInsights.getTimeView("week", startDate, endDate);
const timeLine = await EventsInsights.getTimeLine("week", startDate, endDate);
@@ -448,15 +661,42 @@ export const insightsRouter = router({
eventTypeWhere["id"] = eventTypeId;
}
const bookingWhere: Prisma.BookingWhereInput = {
eventType: eventTypeWhere,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
},
};
if (teamId) {
const usersFromTeam = await ctx.prisma.membership.findMany({
where: {
teamId,
},
select: {
userId: true,
},
});
const userIdsFromTeams = usersFromTeam.map((u) => u.userId);
delete bookingWhere.eventType;
bookingWhere["OR"] = [
{
eventType: {
teamId,
},
},
{
userId: {
in: userIdsFromTeams,
},
},
];
}
const bookingsFromTeam = await ctx.prisma.booking.groupBy({
by: ["userId"],
where: {
eventType: eventTypeWhere,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
},
},
where: bookingWhere,
_count: {
id: true,
},
@@ -467,9 +707,13 @@ export const insightsRouter = router({
},
take: 10,
});
const userIds = bookingsFromTeam
.filter((booking) => typeof booking.userId === "number")
.map((booking) => booking.userId);
if (userIds.length === 0) {
return [];
}
const usersFromTeam = await ctx.prisma.user.findMany({
where: {
id: {
@@ -487,10 +731,11 @@ export const insightsRouter = router({
return {
userId: booking.userId,
user: userHashMap.get(booking.userId),
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
count: booking._count.id,
};
});
return result;
}),
membersWithLeastBookings: userBelongsToTeamProcedure
@@ -507,7 +752,7 @@ export const insightsRouter = router({
if (!teamId) {
return [];
}
const user = ctx.user;
const eventTypeWhere: Prisma.EventTypeWhereInput = {
teamId: teamId,
};
@@ -515,15 +760,42 @@ export const insightsRouter = router({
eventTypeWhere["id"] = eventTypeId;
}
const bookingWhere: Prisma.BookingWhereInput = {
eventType: eventTypeWhere,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
},
};
if (teamId) {
const usersFromTeam = await ctx.prisma.membership.findMany({
where: {
teamId,
},
select: {
userId: true,
},
});
const userIdsFromTeams = usersFromTeam.map((u) => u.userId);
delete bookingWhere.eventType;
bookingWhere["OR"] = [
{
eventType: {
teamId,
},
},
{
userId: {
in: userIdsFromTeams,
},
},
];
}
const bookingsFromTeam = await ctx.prisma.booking.groupBy({
by: ["userId"],
where: {
eventType: eventTypeWhere,
createdAt: {
gte: dayjs(startDate).startOf("day").toDate(),
lte: dayjs(endDate).endOf("day").toDate(),
},
},
where: bookingWhere,
_count: {
id: true,
},
@@ -535,53 +807,31 @@ export const insightsRouter = router({
take: 10,
});
// Users are obtained from bookings so if a user has 0 they won't be in the list
const userIds = bookingsFromTeam
.filter((booking) => typeof booking.userId === "number")
.map((booking) => booking.userId);
const usersWithNoBookings = await ctx.prisma.user.findMany({
select: UserSelect,
if (userIds.length === 0) {
return [];
}
const usersFromTeam = await ctx.prisma.user.findMany({
where: {
id: {
notIn: userIds as number[],
},
teams: {
some: {
teamId: teamId,
},
in: userIds as number[],
},
},
});
let usersFromTeam: Prisma.UserGetPayload<{
select: typeof UserSelect;
}>[] = [];
if (usersWithNoBookings.length < 10) {
usersFromTeam = await ctx.prisma.user.findMany({
where: {
id: {
in: userIds as number[],
},
},
select: UserSelect,
take: 10 - usersWithNoBookings.length,
});
}
const userHashMap = new Map();
[...usersWithNoBookings, ...usersFromTeam].forEach((user) => {
usersFromTeam.forEach((user) => {
userHashMap.set(user.id, user);
});
const result = bookingsFromTeam.map((booking) => {
const user = userHashMap.get(booking.userId);
return {
userId: booking.userId,
user,
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
user: userHashMap.get(booking.userId),
emailMd5: crypto.createHash("md5").update(user?.email).digest("hex"),
count: booking._count.id,
Username: user.name || "No Username found",
};
});
@@ -590,6 +840,18 @@ export const insightsRouter = router({
teamListForUser: authedProcedure.query(async ({ ctx }) => {
const user = ctx.user;
// Fetch user data
const userData = await ctx.prisma.user.findUnique({
where: {
id: user.id,
},
select: {
id: true,
name: true,
avatar: true,
},
});
// Look if user it's admin in multiple teams
const belongsToTeams = await ctx.prisma.membership.findMany({
where: {
@@ -617,8 +879,25 @@ export const insightsRouter = router({
},
},
});
const result = belongsToTeams.map((membership) => membership.team);
const result: {
id: number;
slug: string | null;
name: string | null;
logo: string | null;
userId?: number;
}[] = belongsToTeams.map((membership) => {
return { ...membership.team };
});
if (userData && userData.id) {
result.push({
id: 0,
slug: "",
userId: userData.id,
name: userData.name,
logo: userData.avatar,
});
}
return result;
}),
userList: userBelongsToTeamProcedure
@@ -665,58 +944,84 @@ export const insightsRouter = router({
eventTypeList: userBelongsToTeamProcedure
.input(
z.object({
teamId: z.coerce.number().nullable(),
teamId: z.coerce.number().optional().nullable(),
userId: z.coerce.number().optional().nullable(),
})
)
.query(async ({ ctx, input }) => {
const user = ctx.user;
const { prisma, user } = ctx;
const { teamId, userId } = input;
if (!input.teamId) {
if (!teamId && !userId) {
return [];
}
const membership = await ctx.prisma.membership.findFirst({
where: {
userId: user.id,
teamId: input.teamId,
},
});
if (membership && membership.role === "MEMBER") {
const eventTypes = await ctx.prisma.eventType.findMany({
where: {
teamId: input.teamId,
// user its listed as direct user or as part of a group
OR: [
{
userId: user.id,
},
{
users: {
some: {
id: user.id,
},
},
},
],
},
});
return eventTypes;
const membershipWhereConditional: Prisma.MembershipWhereInput = {};
if (teamId) {
membershipWhereConditional["teamId"] = teamId;
membershipWhereConditional["userId"] = user.id;
}
if (userId) {
membershipWhereConditional["userId"] = userId;
}
const eventTypes = await ctx.prisma.eventType.findMany({
select: {
id: true,
slug: true,
teamId: true,
title: true,
},
where: {
teamId: input.teamId,
},
// I'm not using unique here since when userId comes from input we should look for every
// event type that user owns
const membership = await prisma.membership.findFirst({
where: membershipWhereConditional,
});
return eventTypes;
if (!membership) {
throw new Error("User is not part of a team");
}
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {};
if (teamId) {
eventTypeWhereConditional["teamId"] = teamId;
}
if (userId) {
eventTypeWhereConditional["userId"] = userId;
}
let eventTypeResult: Prisma.EventTypeGetPayload<{
select: {
id: true;
slug: true;
teamId: true;
title: true;
};
}>[] = [];
switch (membership?.role) {
case "MEMBER":
eventTypeWhereConditional["OR"] = {
userId: user.id,
users: { some: { id: user.id } },
// @TODO this is not working as expected
// hosts: { some: { id: user.id } },
};
eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
teamId: true,
title: true,
},
where: eventTypeWhereConditional,
});
break;
default:
eventTypeResult = await prisma.eventType.findMany({
select: {
id: true,
slug: true,
teamId: true,
title: true,
},
where: eventTypeWhereConditional,
});
break;
}
return eventTypeResult;
}),
});
@@ -46,6 +46,7 @@ function getZapierPayload(data: CalendarEvent & EventTypeInfo & { status?: strin
description: data.description,
customInputs: data.customInputs,
responses: data.responses,
userFieldsResponses: data.userFieldsResponses,
startTime: data.startTime,
endTime: data.endTime,
location: location,
@@ -14,7 +14,6 @@ type BookingSelect = {
};
};
location: true;
smsReminderNumber: true;
};
// Backward Compatibility for booking created before we had managed booking questions
@@ -104,4 +103,5 @@ export const getBookingWithResponses = <
responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)),
} as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> };
};
export default getBooking;
+115 -36
View File
@@ -2,221 +2,300 @@
{
"/*": "This file is auto-generated and updated by `yarn app-store create/edit`. Don't edit it manually",
"dirName": "routing-forms",
"categories": ["other"],
"categories": [
"other"
],
"slug": "routing-forms",
"type": "routing-forms_other"
},
{
"dirName": "whereby",
"categories": ["video"],
"categories": [
"video"
],
"slug": "whereby",
"type": "whereby_video"
},
{
"dirName": "around",
"categories": ["video"],
"categories": [
"video"
],
"slug": "around",
"type": "around_video"
},
{
"dirName": "riverside",
"categories": ["video"],
"categories": [
"video"
],
"slug": "riverside",
"type": "riverside_video"
},
{
"dirName": "typeform",
"categories": ["other"],
"categories": [
"other"
],
"slug": "typeform",
"type": "typeform_other"
},
{
"dirName": "ping",
"categories": ["video"],
"categories": [
"video"
],
"slug": "ping",
"type": "ping_video"
},
{
"dirName": "campfire",
"categories": ["video"],
"categories": [
"video"
],
"slug": "campfire",
"type": "campfire_video"
},
{
"dirName": "rainbow",
"categories": ["web3"],
"categories": [
"web3"
],
"slug": "rainbow",
"type": "rainbow_web3"
},
{
"dirName": "raycast",
"categories": ["other"],
"categories": [
"other"
],
"slug": "raycast",
"type": "raycast_other"
},
{
"dirName": "n8n",
"categories": ["automation"],
"categories": [
"automation"
],
"slug": "n8n",
"type": "n8n_automation"
},
{
"dirName": "exchangecalendar",
"categories": ["calendar"],
"categories": [
"calendar"
],
"slug": "exchange",
"type": "exchange_calendar"
},
{
"dirName": "qr_code",
"categories": ["other"],
"categories": [
"other"
],
"slug": "qr_code",
"type": "qr_code_other"
},
{
"dirName": "weather_in_your_calendar",
"categories": ["other"],
"categories": [
"other"
],
"slug": "weather_in_your_calendar",
"type": "weather_in_your_calendar_other"
},
{
"dirName": "fathom",
"categories": ["analytics"],
"categories": [
"analytics"
],
"slug": "fathom",
"type": "fathom_analytics"
},
{
"dirName": "plausible",
"categories": ["analytics"],
"categories": [
"analytics"
],
"slug": "plausible",
"type": "plausible_analytics"
},
{
"dirName": "wordpress",
"categories": ["other"],
"categories": [
"other"
],
"slug": "wordpress",
"type": "wordpress_other"
},
{
"dirName": "ga4",
"categories": ["analytics"],
"categories": [
"analytics"
],
"slug": "ga4",
"type": "ga4_analytics"
},
{
"dirName": "pipedream",
"categories": ["automation"],
"categories": [
"automation"
],
"slug": "pipedream",
"type": "pipedream_automation"
},
{
"dirName": "sirius_video",
"categories": ["video"],
"categories": [
"video"
],
"slug": "sirius_video",
"type": "sirius_video_video"
},
{
"dirName": "sendgrid",
"categories": ["other"],
"categories": [
"other"
],
"slug": "sendgrid",
"type": "sendgrid_other_calendar"
},
{
"dirName": "closecom",
"categories": ["other"],
"categories": [
"other"
],
"slug": "closecom",
"type": "closecom_other_calendar"
},
{
"dirName": "whatsapp",
"categories": ["video"],
"categories": [
"video"
],
"slug": "whatsapp",
"type": "whatsapp_video"
},
{
"dirName": "telegram",
"categories": ["video"],
"categories": [
"video"
],
"slug": "telegram",
"type": "telegram_video"
},
{
"dirName": "signal",
"categories": ["video"],
"categories": [
"video"
],
"slug": "signal",
"type": "signal_video"
},
{
"dirName": "vimcal",
"categories": ["calendar"],
"categories": [
"calendar"
],
"slug": "vimcal",
"type": "vimcal_other"
},
{
"dirName": "amie",
"categories": ["calendar"],
"categories": [
"calendar"
],
"slug": "amie",
"type": "amie_other"
},
{
"dirName": "booking-pages-tag",
"categories": ["analytics"],
"categories": [
"analytics"
],
"slug": "booking-pages-tag",
"type": "booking-pages-tag_other",
"isTemplate": true
},
{
"dirName": "event-type-app-card",
"categories": ["other"],
"categories": [
"other"
],
"slug": "event-type-app-card",
"type": "event-type-app-card_other",
"isTemplate": true
},
{
"dirName": "event-type-location-video-static",
"categories": ["video"],
"categories": [
"video"
],
"slug": "event-type-location-video-static",
"type": "event-type-location-video-static_other",
"isTemplate": true
},
{
"dirName": "general-app-settings",
"categories": ["other"],
"categories": [
"other"
],
"slug": "general-app-settings",
"type": "general-app-settings_other",
"isTemplate": true
},
{
"dirName": "link-as-an-app",
"categories": ["other"],
"categories": [
"other"
],
"slug": "link-as-an-app",
"type": "link-as-an-app_other",
"isTemplate": true
},
{
"dirName": "basic",
"categories": ["other"],
"categories": [
"other"
],
"slug": "basic",
"type": "basic_other",
"isTemplate": true
},
{
"dirName": "facetime",
"categories": ["video"],
"categories": [
"video"
],
"slug": "facetime",
"type": "facetime_video",
"isTemplate": false
},
{
"dirName": "zohocrm",
"categories": ["other"],
"categories": [
"other"
],
"slug": "zohocrm",
"type": "zohocrm_other_calendar",
"isTemplate": false
},
{
"dirName": "cron",
"categories": ["calendar"],
"categories": [
"calendar"
],
"slug": "cron",
"type": "cron_other",
"isTemplate": false
},
{
"dirName": "google-tag-manager",
"categories": [
"analytics"
],
"slug": "google-tag-manager",
"type": "google-tag-manager_analytics",
"isTemplate": false
}
]
]
+1
View File
@@ -232,6 +232,7 @@ export default async function main() {
});
}
await createApp("jitsi", "jitsivideo", ["video"], "jitsi_video");
await createApp("sylaps", "sylapsvideo", ["video"], "sylaps_video");
// Other apps
if (process.env.HUBSPOT_CLIENT_ID && process.env.HUBSPOT_CLIENT_SECRET) {
await createApp("hubspot", "hubspot", ["other"], "hubspot_other_calendar", {
+234 -74
View File
@@ -5,6 +5,65 @@ import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { hashPassword } from "@calcom/features/auth/lib/hashPassword";
const shuffle = (
booking: any,
year: number,
eventTypesToPick: Prisma.EventTypeGetPayload<{
select: {
id: true;
userId: true;
title: true;
length: true;
description: true;
};
}>[],
usersIdsToPick?: number[]
) => {
const startTime = dayjs(booking.startTime)
.add(Math.floor(Math.random() * 365), "day")
.add(Math.floor(Math.random() * 24), "hour")
.add(Math.floor(Math.random() * 60), "minute")
.set("y", year);
const randomEvent = eventTypesToPick[Math.floor(Math.random() * eventTypesToPick.length)];
const endTime = dayjs(startTime).add(Math.floor(Math.random() * randomEvent.length), "minute");
booking.title = randomEvent.title;
booking.description = randomEvent.description;
booking.startTime = startTime.toISOString();
booking.endTime = endTime.toISOString();
booking.createdAt = startTime.subtract(1, "day").toISOString();
// Pick a random status
const randomStatusIndex = Math.floor(Math.random() * Object.keys(BookingStatus).length);
const statusKey = Object.keys(BookingStatus)[randomStatusIndex];
booking.status = BookingStatus[statusKey];
booking.rescheduled = Math.random() > 0.5 && Math.random() > 0.5 && Math.random() > 0.5;
if (booking.rescheduled) {
booking.status = "CANCELLED";
}
const randomEventTypeId = randomEvent.id;
booking.eventTypeId = randomEventTypeId;
booking.uid = uuidv4();
if (usersIdsToPick && usersIdsToPick.length > 0) {
booking.userId = Math.random() > 0.5 ? usersIdsToPick[0] : usersIdsToPick[1];
} else {
booking.userId = randomEvent.userId;
}
if (booking.userId === undefined || booking.userId === null) {
console.log({ randomEvent, usersIdsToPick });
console.log("This should not happen");
}
return booking;
};
const prisma = new PrismaClient();
async function main() {
// First find if not then create everything
@@ -18,25 +77,30 @@ async function main() {
insightsAdmin = await prisma.user.create({
data: {
email: "insights@example.com",
password: await hashPassword("insights"),
password: await hashPassword("insightsinsightsinsights...!"),
name: "Insights Admin",
role: "ADMIN",
username: "insights",
username: "insights-admin",
completedOnboarding: true,
},
});
if (!insightsAdmin) {
// Throw error
throw new Error("Insights Admin not created");
}
}
let insightsUser = await prisma.user.findFirst({
where: {
email: "insightuser@example.com",
email: "insightsuser@example.com",
},
});
if (!insightsUser) {
insightsUser = await prisma.user.create({
data: {
email: "insightuser@example.com",
email: "insightsuser@example.com",
password: await hashPassword("insightsuser"),
name: "Insights User",
role: "USER",
@@ -44,6 +108,10 @@ async function main() {
completedOnboarding: true,
},
});
if (!insightsUser) {
// Throw error
throw new Error("Insights Admin not created");
}
}
let insightsTeam = await prisma.team.findFirst({
@@ -75,37 +143,33 @@ async function main() {
},
],
});
await prisma.team.update({
where: {
id: insightsTeam.id,
},
data: {
members: {
connect: {
userId_teamId: {
userId: insightsAdmin.id,
teamId: insightsTeam.id,
if (!insightsTeam) {
insightsTeam = await prisma.team.create({
data: {
name: "Insights",
slug: "insights-team",
},
});
}
if (insightsAdmin && insightsTeam) {
await prisma.team.update({
where: {
id: insightsTeam.id,
},
data: {
members: {
connect: {
userId_teamId: {
userId: insightsAdmin.id,
teamId: insightsTeam.id,
},
},
},
},
},
});
});
}
}
await prisma.team.update({
where: {
id: insightsTeam.id,
},
data: {
members: {
connect: {
userId_teamId: {
userId: insightsUser.id,
teamId: insightsTeam.id,
},
},
},
},
});
let teamEvents = await prisma.eventType.findMany({
where: {
@@ -113,6 +177,112 @@ async function main() {
},
});
const userSingleEventsAdmin = await prisma.eventType.findMany({
where: {
userId: insightsUser.id,
},
});
const userSingleEvents = await prisma.eventType.findMany({
where: {
userId: insightsUser.id,
},
});
if (userSingleEventsAdmin.length === 0 && userSingleEvents.length === 0) {
await prisma.eventType.create({
data: {
title: "Single Event Admin",
slug: "single-event-admin",
description: "Single Event Admin",
length: 60,
userId: insightsAdmin.id,
users: {
connect: {
id: insightsAdmin.id,
},
},
},
});
await prisma.eventType.create({
data: {
title: "Single Event",
slug: "single-event",
description: "Single Event",
length: 60,
userId: insightsUser.id,
users: {
connect: {
id: insightsUser.id,
},
},
},
});
const singleEventTypesAdmin = await prisma.eventType.findFirst({
select: {
id: true,
userId: true,
title: true,
length: true,
description: true,
},
where: {
userId: insightsAdmin.id,
},
});
const singleEventTypes = await prisma.eventType.findFirst({
select: {
id: true,
userId: true,
length: true,
title: true,
description: true,
},
where: {
userId: insightsUser.id,
},
});
if (!singleEventTypesAdmin || !singleEventTypes) {
throw new Error("Could not create single event types");
}
const singleEvents = [singleEventTypesAdmin, singleEventTypes];
// create bookings random for single event type
const baseBookingSingle: Prisma.BookingCreateManyInput = {
uid: "demoUIDSingle",
title: "Single Event",
description: "Single Event",
startTime: dayjs().toISOString(),
endTime: dayjs().toISOString(),
// This gets overriden in shuffle
userId: insightsUser.id,
};
// Validate if they are null of undefined throw error
if (!insightsAdmin || !insightsUser || !insightsAdmin.id || !insightsUser.id) {
throw new Error("Could not find user");
}
await prisma.booking.createMany({
data: [
...new Array(100)
.fill(0)
.map(() => shuffle({ ...baseBookingSingle }, dayjs().get("y") - 1, singleEvents)),
],
});
await prisma.booking.createMany({
data: [
...new Array(100)
.fill(0)
.map(() => shuffle({ ...baseBookingSingle }, dayjs().get("y") - 0, singleEvents)),
],
});
}
if (teamEvents.length === 0) {
await prisma.eventType.createMany({
data: [
@@ -142,7 +312,6 @@ async function main() {
},
],
});
} else {
teamEvents = await prisma.eventType.findMany({
where: {
teamId: insightsTeam.id,
@@ -152,61 +321,52 @@ async function main() {
const baseBooking: Prisma.BookingCreateManyInput = {
uid: "demoUID",
title: "Team Meeting",
description: "Team Meeting",
title: "Team Meeting should be changed in shuffle",
description: "Team Meeting Should be changed in shuffle",
startTime: dayjs().toISOString(),
endTime: dayjs().toISOString(),
userId: insightsUser.id,
eventTypeId: teamEvents[0].id,
};
const shuffle = (booking: typeof baseBooking, year) => {
const startTime = dayjs(booking.startTime)
.add(Math.floor(Math.random() * 365), "day")
.add(Math.floor(Math.random() * 24), "hour")
.add(Math.floor(Math.random() * 60), "minute")
.set("y", year);
const randomEvent = teamEvents[Math.floor(Math.random() * teamEvents.length)];
const endTime = dayjs(startTime).add(Math.floor(Math.random() * randomEvent.length), "minute");
booking.startTime = startTime.toISOString();
booking.endTime = endTime.toISOString();
booking.createdAt = startTime.subtract(1, "day").toISOString();
// Pick a random status
const randomStatusIndex = Math.floor(Math.random() * Object.keys(BookingStatus).length);
const statusKey = Object.keys(BookingStatus)[randomStatusIndex];
booking.status = BookingStatus[statusKey];
booking.rescheduled = Math.random() > 0.5 && Math.random() > 0.5 && Math.random() > 0.5;
if (booking.rescheduled) {
booking.status = "CANCELLED";
}
const randomEventTypeId = teamEvents[Math.floor(Math.random() * teamEvents.length)].id;
booking.eventTypeId = randomEventTypeId;
booking.uid = uuidv4();
if (insightsUser && insightsAdmin) {
booking.userId = Math.random() > 0.5 ? insightsUser.id : insightsAdmin.id;
}
return booking;
};
// Create past bookings
// Create past bookings -2y, -1y, -0y
await prisma.booking.createMany({
data: [...new Array(100).fill(0).map(() => shuffle({ ...baseBooking }, dayjs().get("y") - 2))],
data: [
...new Array(100)
.fill(0)
.map(() =>
shuffle({ ...baseBooking }, dayjs().get("y") - 2, teamEvents, [
insightsAdmin?.id ?? 0,
insightsUser?.id ?? 0,
])
),
],
});
await prisma.booking.createMany({
data: [...new Array(100).fill(0).map(() => shuffle({ ...baseBooking }, dayjs().get("y") - 1))],
data: [
...new Array(100)
.fill(0)
.map(() =>
shuffle({ ...baseBooking }, dayjs().get("y") - 1, teamEvents, [
insightsAdmin?.id ?? 0,
insightsUser?.id ?? 0,
])
),
],
});
await prisma.booking.createMany({
data: [...new Array(100).fill(0).map(() => shuffle({ ...baseBooking }, dayjs().get("y")))],
data: [
...new Array(100)
.fill(0)
.map(() =>
shuffle({ ...baseBooking }, dayjs().get("y"), teamEvents, [
insightsAdmin?.id ?? 0,
insightsUser?.id ?? 0,
])
),
],
});
}
main()
+36 -3
View File
@@ -1,6 +1,5 @@
import type { DestinationCalendar, Prisma } from "@prisma/client";
import { AppCategories, BookingStatus, IdentityProvider } from "@prisma/client";
import { cityMapping } from "city-timezones";
import { reverse } from "lodash";
import type { NextApiResponse } from "next";
import { authenticator } from "otplib";
@@ -23,6 +22,7 @@ import dayjs from "@calcom/dayjs";
import { sendCancelledEmails, sendFeedbackEmail } from "@calcom/emails";
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
import { verifyPassword } from "@calcom/features/auth/lib/verifyPassword";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { samlTenantProduct } from "@calcom/features/ee/sso/lib/saml";
import { featureFlagRouter } from "@calcom/features/flags/server/router";
import { insightsRouter } from "@calcom/features/insights/server/trpc-router";
@@ -159,7 +159,35 @@ const publicViewerRouter = router({
}),
// REVIEW: This router is part of both the public and private viewer router?
slots: slotsRouter,
cityTimezones: publicProcedure.query(() => cityMapping),
cityTimezones: publicProcedure.query(async () => {
/**
* Lazy loads third party dependency to avoid loading 1.5Mb for ALL tRPC procedures.
* Thanks @roae for the tip 🙏
**/
const allCities = await import("city-timezones").then((mod) => mod.cityMapping);
/**
* Filter out all cities that have the same "city" key and only use the one with the highest population.
* This way we return a new array of cities without running the risk of having more than one city
* with the same name on the dropdown and prevent users from mistaking the time zone of the desired city.
*/
const topPopulatedCities: { [key: string]: { city: string; timezone: string; pop: number } } = {};
allCities.forEach((city) => {
const cityPopulationCount = city.pop;
if (
topPopulatedCities[city.city]?.pop === undefined ||
cityPopulationCount > topPopulatedCities[city.city].pop
) {
topPopulatedCities[city.city] = { city: city.city, timezone: city.timezone, pop: city.pop };
}
});
const uniqueCities = Object.values(topPopulatedCities);
/** Add specific overries in here */
uniqueCities.forEach((city) => {
if (city.city === "London") city.timezone = "Europe/London";
if (city.city === "Londonderry") city.city = "London";
});
return uniqueCities;
}),
});
// routes only available to authenticated users
@@ -1008,6 +1036,7 @@ const loggedInViewerRouter = router({
...bookingMinimalSelect,
recurringEventId: true,
userId: true,
responses: true,
user: {
select: {
id: true,
@@ -1033,6 +1062,7 @@ const loggedInViewerRouter = router({
select: {
recurringEvent: true,
title: true,
bookingFields: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
},
@@ -1093,12 +1123,15 @@ const loggedInViewerRouter = router({
const attendeesList = await Promise.all(attendeesListPromises);
const tOrganizer = await getTranslation(booking?.user?.locale ?? "en", "common");
await sendCancelledEmails({
type: booking?.eventType?.title as string,
title: booking.title,
description: booking.description,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
organizer: {
@@ -15,8 +15,6 @@ import dayjs from "@calcom/dayjs";
import { deleteScheduledEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
import { deleteScheduledSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
import { sendDeclinedEmails, sendLocationChangeEmails, sendRequestRescheduleEmail } from "@calcom/emails";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
@@ -418,6 +416,7 @@ export const bookingsRouter = router({
smsReminderNumber: true,
scheduledJobs: true,
workflowReminders: true,
responses: true,
},
where: {
uid: bookingId,
@@ -577,6 +576,10 @@ export const bookingsRouter = router({
type: event && event.title ? event.title : bookingToReschedule.title,
description: bookingToReschedule?.description || "",
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
...getCalEventResponses({
booking: bookingToReschedule,
bookingFields: bookingToReschedule.eventType?.bookingFields ?? null,
}),
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
organizer: userAsPeopleType,
@@ -722,7 +725,7 @@ export const bookingsRouter = router({
const tOrganizer = await getTranslation(user.locale ?? "en", "common");
const bookingRaw = await prisma.booking.findUniqueOrThrow({
const booking = await prisma.booking.findUniqueOrThrow({
where: {
id: bookingId,
},
@@ -776,21 +779,6 @@ export const bookingsRouter = router({
},
});
const bookingFields = bookingRaw.eventType
? getBookingFieldsWithSystemFields(bookingRaw.eventType)
: null;
const booking = {
...bookingRaw,
responses: bookingResponsesDbSchema.parse(bookingRaw.responses),
eventType: bookingRaw.eventType
? {
...bookingRaw.eventType,
bookingFields,
}
: null,
};
const authorized = async () => {
// if the organizer
if (booking.userId === user.id) {
@@ -848,18 +836,15 @@ export const bookingsRouter = router({
const attendeesList = await Promise.all(attendeesListPromises);
// TODO: Remove the usage of `bookingFields` in computing responses. We can do that by storing `label` with the response. Also, this would allow us to correctly show the label for a field even after the Event Type has been deleted.
const { calEventUserFieldsResponses, calEventResponses } = getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
responses: booking.responses,
});
const evt: CalendarEvent = {
type: booking.eventType?.title || booking.title,
title: booking.title,
description: booking.description,
responses: calEventResponses,
userFieldsResponses: calEventUserFieldsResponses,
// TODO: Remove the usage of `bookingFields` in computing responses. We can do that by storing `label` with the response. Also, this would allow us to correctly show the label for a field even after the Event Type has been deleted.
...getCalEventResponses({
bookingFields: booking.eventType?.bookingFields ?? null,
booking,
}),
customInputs: isPrismaObjOrUndefined(booking.customInputs),
startTime: booking.startTime.toISOString(),
endTime: booking.endTime.toISOString(),
+449 -130
View File
@@ -122,25 +122,6 @@ __metadata:
languageName: node
linkType: hard
"@auth/core@npm:^0.1.4":
version: 0.1.4
resolution: "@auth/core@npm:0.1.4"
dependencies:
"@panva/hkdf": 1.0.2
cookie: 0.5.0
jose: 4.11.1
oauth4webapi: 2.0.5
preact: 10.11.3
preact-render-to-string: 5.2.3
peerDependencies:
nodemailer: 6.8.0
peerDependenciesMeta:
nodemailer:
optional: true
checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79
languageName: node
linkType: hard
"@aws-crypto/ie11-detection@npm:^3.0.0":
version: 3.0.0
resolution: "@aws-crypto/ie11-detection@npm:3.0.0"
@@ -3893,34 +3874,27 @@ __metadata:
version: 0.0.0-use.local
resolution: "@calcom/api@workspace:apps/api"
dependencies:
"@calcom/app-store": "*"
"@calcom/core": "*"
"@calcom/dayjs": "*"
"@calcom/emails": "*"
"@calcom/embed-core": "workspace:*"
"@calcom/embed-snippet": "workspace:*"
"@calcom/features": "*"
"@calcom/lib": "*"
"@calcom/prisma": "*"
"@calcom/trpc": "*"
"@calcom/tsconfig": "*"
"@calcom/types": "*"
"@sentry/nextjs": ^7.20.0
"@sentry/nextjs": ^6.19.7
babel-jest: ^28.1.0
bcryptjs: ^2.4.3
jest: ^28.1.0
memory-cache: ^0.2.0
modify-response-middleware: ^1.1.0
next: ^13.2.1
next: ^12.2.5
next-api-middleware: ^1.0.1
next-axiom: ^0.16.0
next-axiom: ^0.10.0
next-swagger-doc: ^0.3.4
next-transpile-modules: ^9.0.0
next-validations: ^0.2.0
node-mocks-http: ^1.11.0
typescript: ^4.9.4
typescript: ^4.7.4
tzdata: ^1.0.30
uuid: ^8.3.2
zod: ^3.20.2
zod: ^3.18.0
languageName: unknown
linkType: soft
@@ -3982,39 +3956,6 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/auth@workspace:apps/auth":
version: 0.0.0-use.local
resolution: "@calcom/auth@workspace:apps/auth"
dependencies:
"@auth/core": ^0.1.4
"@calcom/app-store": "*"
"@calcom/app-store-cli": "*"
"@calcom/config": "*"
"@calcom/core": "*"
"@calcom/dayjs": "*"
"@calcom/embed-core": "workspace:*"
"@calcom/embed-react": "workspace:*"
"@calcom/embed-snippet": "workspace:*"
"@calcom/features": "*"
"@calcom/lib": "*"
"@calcom/prisma": "*"
"@calcom/trpc": "*"
"@calcom/tsconfig": "*"
"@calcom/types": "*"
"@calcom/ui": "*"
"@types/node": 16.9.1
"@types/react": 18.0.26
"@types/react-dom": 18.0.9
eslint: ^8.34.0
eslint-config-next: ^13.2.1
next: ^13.2.1
next-auth: ^4.20.1
react: ^18.2.0
react-dom: ^18.2.0
typescript: ^4.9.4
languageName: unknown
linkType: soft
"@calcom/caldavcalendar@workspace:packages/app-store/caldavcalendar":
version: 0.0.0-use.local
resolution: "@calcom/caldavcalendar@workspace:packages/app-store/caldavcalendar"
@@ -4699,6 +4640,15 @@ __metadata:
languageName: unknown
linkType: soft
"@calcom/sylapsvideo@workspace:packages/app-store/sylapsvideo":
version: 0.0.0-use.local
resolution: "@calcom/sylapsvideo@workspace:packages/app-store/sylapsvideo"
dependencies:
"@calcom/lib": "*"
"@calcom/types": "*"
languageName: unknown
linkType: soft
"@calcom/tandemvideo@workspace:packages/app-store/tandemvideo":
version: 0.0.0-use.local
resolution: "@calcom/tandemvideo@workspace:packages/app-store/tandemvideo"
@@ -7919,6 +7869,13 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:12.3.4":
version: 12.3.4
resolution: "@next/env@npm:12.3.4"
checksum: daa3fc11efd1344c503eab41311a0e503ba7fd08607eeb3dc571036a6211eb37959cc4ed48b71dcc411cc214e7623ffd02411080aad3e09dc6a1192d5b256e60
languageName: node
linkType: hard
"@next/env@npm:13.2.3":
version: 13.2.3
resolution: "@next/env@npm:13.2.3"
@@ -7942,6 +7899,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-android-arm-eabi@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-android-arm-eabi@npm:12.3.4"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@next/swc-android-arm-eabi@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-android-arm-eabi@npm:13.2.3"
@@ -7956,6 +7920,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-android-arm64@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-android-arm64@npm:12.3.4"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@next/swc-android-arm64@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-android-arm64@npm:13.2.3"
@@ -7970,6 +7941,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-darwin-arm64@npm:12.3.4"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-darwin-arm64@npm:13.2.3"
@@ -7984,6 +7962,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-darwin-x64@npm:12.3.4"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-darwin-x64@npm:13.2.3"
@@ -7998,6 +7983,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-freebsd-x64@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-freebsd-x64@npm:12.3.4"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@next/swc-freebsd-x64@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-freebsd-x64@npm:13.2.3"
@@ -8012,6 +8004,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-linux-arm-gnueabihf@npm:12.3.4"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-linux-arm-gnueabihf@npm:13.2.3"
@@ -8026,6 +8025,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-linux-arm64-gnu@npm:12.3.4"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-linux-arm64-gnu@npm:13.2.3"
@@ -8040,6 +8046,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-linux-arm64-musl@npm:12.3.4"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-linux-arm64-musl@npm:13.2.3"
@@ -8054,6 +8067,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-linux-x64-gnu@npm:12.3.4"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-linux-x64-gnu@npm:13.2.3"
@@ -8068,6 +8088,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-linux-x64-musl@npm:12.3.4"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-linux-x64-musl@npm:13.2.3"
@@ -8082,6 +8109,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-win32-arm64-msvc@npm:12.3.4"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-win32-arm64-msvc@npm:13.2.3"
@@ -8096,6 +8130,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-win32-ia32-msvc@npm:12.3.4"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-win32-ia32-msvc@npm:13.2.3"
@@ -8110,6 +8151,13 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:12.3.4":
version: 12.3.4
resolution: "@next/swc-win32-x64-msvc@npm:12.3.4"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.2.3":
version: 13.2.3
resolution: "@next/swc-win32-x64-msvc@npm:13.2.3"
@@ -8269,13 +8317,6 @@ __metadata:
languageName: node
linkType: hard
"@panva/hkdf@npm:1.0.2":
version: 1.0.2
resolution: "@panva/hkdf@npm:1.0.2"
checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03
languageName: node
linkType: hard
"@panva/hkdf@npm:^1.0.2":
version: 1.0.4
resolution: "@panva/hkdf@npm:1.0.4"
@@ -8391,14 +8432,14 @@ __metadata:
languageName: node
linkType: hard
"@prisma/debug@npm:4.11.0":
version: 4.11.0
resolution: "@prisma/debug@npm:4.11.0"
"@prisma/debug@npm:4.12.0":
version: 4.12.0
resolution: "@prisma/debug@npm:4.12.0"
dependencies:
"@types/debug": 4.1.7
debug: 4.3.4
strip-ansi: 6.0.1
checksum: 170adb225d6e8e0e0df26d441d550607c7fecb97969b47ca19264d65636564c774ba60b9cefab196bb86d358cdd06a289e2ad8d3f70201cb3becb79547343bf5
checksum: 0ae182634f4a25231ff946768e1da1f694b5141d21c92cb2a142580de30d67ebd1595f8575a4c63356740e415d5d64829449a4710d8421e7147016ea0acb2643
languageName: node
linkType: hard
@@ -8417,14 +8458,14 @@ __metadata:
linkType: hard
"@prisma/generator-helper@npm:^4.0.0":
version: 4.11.0
resolution: "@prisma/generator-helper@npm:4.11.0"
version: 4.12.0
resolution: "@prisma/generator-helper@npm:4.12.0"
dependencies:
"@prisma/debug": 4.11.0
"@prisma/debug": 4.12.0
"@types/cross-spawn": 6.0.2
chalk: 4.1.2
cross-spawn: 7.0.3
checksum: 4d8cb919893cc22a743e625c7d077e3cbd67e4541500025d253dfd6df2452d294d05bbd0a0599007c562796badba92d57191ecfb980e3c29c906ba0038d37faf
checksum: b844fcf736c922ce39e6aa1081b7c133b921ceb5d272f51296cf7afc3128ca6085ef1594ce63045ef47a462c56e5e37b9a1094a36d5dd484f77017d6f227641f
languageName: node
linkType: hard
@@ -9850,6 +9891,18 @@ __metadata:
languageName: node
linkType: hard
"@sentry/browser@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/browser@npm:6.19.7"
dependencies:
"@sentry/core": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: 071d00c76c2d0384580474c634c58c6196bbd1a3cf510da1309bd1565c57df7422fca8ceb717db189fa557f2c711a21664ee1ab935dfd9869faf416d388e6f78
languageName: node
linkType: hard
"@sentry/browser@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/browser@npm:7.20.0"
@@ -9862,6 +9915,22 @@ __metadata:
languageName: node
linkType: hard
"@sentry/cli@npm:^1.73.0":
version: 1.75.0
resolution: "@sentry/cli@npm:1.75.0"
dependencies:
https-proxy-agent: ^5.0.0
mkdirp: ^0.5.5
node-fetch: ^2.6.7
progress: ^2.0.3
proxy-from-env: ^1.1.0
which: ^2.0.2
bin:
sentry-cli: bin/sentry-cli
checksum: d5b130bb45bc96d0f738801343455e6a113d1cc9e16166f0d6bfa022809b557159f010677d2dc05adcdfa7f8ad23c401182b6daef720fa2a0bad82fa6e3b2148
languageName: node
linkType: hard
"@sentry/cli@npm:^1.74.6":
version: 1.74.6
resolution: "@sentry/cli@npm:1.74.6"
@@ -9879,6 +9948,19 @@ __metadata:
languageName: node
linkType: hard
"@sentry/core@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/core@npm:6.19.7"
dependencies:
"@sentry/hub": 6.19.7
"@sentry/minimal": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: d212e8ef07114549de4a93b81f8bfa217ca1550ca7a5eeaa611e5629faef78ff72663ce561ffa2cff48f3dc556745ef65177044f9965cdd3cbccf617cf3bf675
languageName: node
linkType: hard
"@sentry/core@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/core@npm:7.20.0"
@@ -9890,6 +9972,29 @@ __metadata:
languageName: node
linkType: hard
"@sentry/hub@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/hub@npm:6.19.7"
dependencies:
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: 10bb1c5cba1b0f1e27a3dd0a186c22f94aeaf11c4662890ab07b2774f46f46af78d61e3ba71d76edc750a7b45af86edd032f35efecdb4efa2eaf551080ccdcb1
languageName: node
linkType: hard
"@sentry/integrations@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/integrations@npm:6.19.7"
dependencies:
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
localforage: ^1.8.1
tslib: ^1.9.3
checksum: 6ea22ef8d518e6ade96ad00c07f5346c8316264ae6ec353c299e03d1d3cf4b226f639bde07dd5d1da28feaa031338e75df25fc60a083a24d847f4cfde4dbc8e9
languageName: node
linkType: hard
"@sentry/integrations@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/integrations@npm:7.20.0"
@@ -9902,6 +10007,41 @@ __metadata:
languageName: node
linkType: hard
"@sentry/minimal@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/minimal@npm:6.19.7"
dependencies:
"@sentry/hub": 6.19.7
"@sentry/types": 6.19.7
tslib: ^1.9.3
checksum: 9153ac426ee056fc34c5be898f83d74ec08f559d69f544c5944ec05e584b62ed356b92d1a9b08993a7022ad42b5661c3d72881221adc19bee5fc1af3ad3864a8
languageName: node
linkType: hard
"@sentry/nextjs@npm:^6.19.7":
version: 6.19.7
resolution: "@sentry/nextjs@npm:6.19.7"
dependencies:
"@sentry/core": 6.19.7
"@sentry/hub": 6.19.7
"@sentry/integrations": 6.19.7
"@sentry/node": 6.19.7
"@sentry/react": 6.19.7
"@sentry/tracing": 6.19.7
"@sentry/utils": 6.19.7
"@sentry/webpack-plugin": 1.18.8
tslib: ^1.9.3
peerDependencies:
next: ^10.0.8 || ^11.0 || ^12.0
react: 15.x || 16.x || 17.x || 18.x
webpack: ">= 4.0.0"
peerDependenciesMeta:
webpack:
optional: true
checksum: f45228d9dd94457db5febca1e7f79b0689000cecc0beb8155724ea69657f871fc0116a3c884be0167196a730856aaf862db7869ef14e16183091ae0607d3a78a
languageName: node
linkType: hard
"@sentry/nextjs@npm:^7.20.0":
version: 7.20.0
resolution: "@sentry/nextjs@npm:7.20.0"
@@ -9930,6 +10070,22 @@ __metadata:
languageName: node
linkType: hard
"@sentry/node@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/node@npm:6.19.7"
dependencies:
"@sentry/core": 6.19.7
"@sentry/hub": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
cookie: ^0.4.1
https-proxy-agent: ^5.0.0
lru_map: ^0.3.3
tslib: ^1.9.3
checksum: 2293b0d1d1f9fac3a451eb94f820bc27721c8edddd1f373064666ddd6272f0a4c70dbe58c6c4b3d3ccaf4578aab8f466d71ee69f6f6ff93521bbb02dfe829ce5
languageName: node
linkType: hard
"@sentry/node@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/node@npm:7.20.0"
@@ -9945,6 +10101,22 @@ __metadata:
languageName: node
linkType: hard
"@sentry/react@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/react@npm:6.19.7"
dependencies:
"@sentry/browser": 6.19.7
"@sentry/minimal": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
hoist-non-react-statics: ^3.3.2
tslib: ^1.9.3
peerDependencies:
react: 15.x || 16.x || 17.x || 18.x
checksum: f49b74c617e3d5094109417a4ab957d4304ec5c3c6d7208c93fe484f263c70c9656259471ae0367b75a303466d7715285ae9ac4876927d6bc285dab4d8e1591d
languageName: node
linkType: hard
"@sentry/react@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/react@npm:7.20.0"
@@ -9960,6 +10132,19 @@ __metadata:
languageName: node
linkType: hard
"@sentry/tracing@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/tracing@npm:6.19.7"
dependencies:
"@sentry/hub": 6.19.7
"@sentry/minimal": 6.19.7
"@sentry/types": 6.19.7
"@sentry/utils": 6.19.7
tslib: ^1.9.3
checksum: 26e7bf435f0478356d95fab50fe4acd2cf449bfdfd457996111aeedd082f6c76f0592abd0c5f5dcfe2b582d3fd7988ffef07787e7ecc7f5d228278731ce6264e
languageName: node
linkType: hard
"@sentry/tracing@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/tracing@npm:7.20.0"
@@ -9972,6 +10157,13 @@ __metadata:
languageName: node
linkType: hard
"@sentry/types@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/types@npm:6.19.7"
checksum: f46ef74a33376ad6ea9b128115515c58eb9369d89293c60aa67abca26b5d5d519aa4d0a736db56ae0d75ffd816643d62187018298523cbc2e6c2fb3a6b2a9035
languageName: node
linkType: hard
"@sentry/types@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/types@npm:7.20.0"
@@ -9979,6 +10171,16 @@ __metadata:
languageName: node
linkType: hard
"@sentry/utils@npm:6.19.7":
version: 6.19.7
resolution: "@sentry/utils@npm:6.19.7"
dependencies:
"@sentry/types": 6.19.7
tslib: ^1.9.3
checksum: a000223b9c646c64e3565e79cace1eeb75114342b768367c4dddd646476c215eb1bddfb70c63f05e2352d3bce2d7d415344e4757a001605d0e01ac74da5dd306
languageName: node
linkType: hard
"@sentry/utils@npm:7.20.0":
version: 7.20.0
resolution: "@sentry/utils@npm:7.20.0"
@@ -9989,6 +10191,15 @@ __metadata:
languageName: node
linkType: hard
"@sentry/webpack-plugin@npm:1.18.8":
version: 1.18.8
resolution: "@sentry/webpack-plugin@npm:1.18.8"
dependencies:
"@sentry/cli": ^1.73.0
checksum: e159ee1e4b486a6d602bcf04e08ce4f167c9148aa4b612f6871f81954fba0cf607297a77bcda22f6f882266ac715ce8cb7be55858eaebc9fdbb85737ae92eb04
languageName: node
linkType: hard
"@sentry/webpack-plugin@npm:1.20.0":
version: 1.20.0
resolution: "@sentry/webpack-plugin@npm:1.20.0"
@@ -11814,6 +12025,15 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.4.11":
version: 0.4.11
resolution: "@swc/helpers@npm:0.4.11"
dependencies:
tslib: ^2.4.0
checksum: 736857d524b41a8a4db81094e9b027f554004e0fa3e86325d85bdb38f7e6459ce022db079edb6c61ba0f46fe8583b3e663e95f7acbd13e51b8da6c34e45bba2e
languageName: node
linkType: hard
"@swc/helpers@npm:0.4.14":
version: 0.4.14
resolution: "@swc/helpers@npm:0.4.14"
@@ -22715,9 +22935,9 @@ __metadata:
linkType: hard
"github-buttons@npm:^2.22.0":
version: 2.24.0
resolution: "github-buttons@npm:2.24.0"
checksum: 3bf27f9078180e36cfc87e214e788898da603d606c871deedc40b7edef4abf3f659a6cff65bd6990c07f59f473468de92174a04a91c42ca95b868d5da1c4d287
version: 2.26.0
resolution: "github-buttons@npm:2.26.0"
checksum: e06e8dd23195201250ba4455e3044f75306fbe76400a3978014a5d7490a24e5e360420f50912e3e00ee5a6fa2ad76afe24ec62f9144d529059bf3c1226498f2a
languageName: node
linkType: hard
@@ -26006,13 +26226,6 @@ __metadata:
languageName: node
linkType: hard
"jose@npm:4.11.1":
version: 4.11.1
resolution: "jose@npm:4.11.1"
checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7
languageName: node
linkType: hard
"jose@npm:4.12.0":
version: 4.12.0
resolution: "jose@npm:4.12.0"
@@ -29294,6 +29507,17 @@ __metadata:
languageName: node
linkType: hard
"next-axiom@npm:^0.10.0":
version: 0.10.0
resolution: "next-axiom@npm:0.10.0"
dependencies:
whatwg-fetch: ^3.6.2
peerDependencies:
next: ^12.1.4
checksum: 57aea4edbdab1a4da59eb16644e850a082a23657128f1f953a2b3601b09fa4558090d42b616f8a3e6022c0deef0d95937321f373caa9ca591178dd8445528921
languageName: node
linkType: hard
"next-axiom@npm:^0.16.0":
version: 0.16.0
resolution: "next-axiom@npm:0.16.0"
@@ -29401,6 +29625,16 @@ __metadata:
languageName: node
linkType: hard
"next-transpile-modules@npm:^9.0.0":
version: 9.1.0
resolution: "next-transpile-modules@npm:9.1.0"
dependencies:
enhanced-resolve: ^5.10.0
escalade: ^3.1.1
checksum: 8cc46196db3c2d2063fb29fe5b4d03c21065ab08130085b24d61e4ed512d99c12083d28179771cf02f70f8bf5970db0781a228aacf6cc61662dbdcabaddfc472
languageName: node
linkType: hard
"next-validations@npm:^0.2.0":
version: 0.2.1
resolution: "next-validations@npm:0.2.1"
@@ -29481,6 +29715,75 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^12.2.5":
version: 12.3.4
resolution: "next@npm:12.3.4"
dependencies:
"@next/env": 12.3.4
"@next/swc-android-arm-eabi": 12.3.4
"@next/swc-android-arm64": 12.3.4
"@next/swc-darwin-arm64": 12.3.4
"@next/swc-darwin-x64": 12.3.4
"@next/swc-freebsd-x64": 12.3.4
"@next/swc-linux-arm-gnueabihf": 12.3.4
"@next/swc-linux-arm64-gnu": 12.3.4
"@next/swc-linux-arm64-musl": 12.3.4
"@next/swc-linux-x64-gnu": 12.3.4
"@next/swc-linux-x64-musl": 12.3.4
"@next/swc-win32-arm64-msvc": 12.3.4
"@next/swc-win32-ia32-msvc": 12.3.4
"@next/swc-win32-x64-msvc": 12.3.4
"@swc/helpers": 0.4.11
caniuse-lite: ^1.0.30001406
postcss: 8.4.14
styled-jsx: 5.0.7
use-sync-external-store: 1.2.0
peerDependencies:
fibers: ">= 3.1.0"
node-sass: ^6.0.0 || ^7.0.0
react: ^17.0.2 || ^18.0.0-0
react-dom: ^17.0.2 || ^18.0.0-0
sass: ^1.3.0
dependenciesMeta:
"@next/swc-android-arm-eabi":
optional: true
"@next/swc-android-arm64":
optional: true
"@next/swc-darwin-arm64":
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-freebsd-x64":
optional: true
"@next/swc-linux-arm-gnueabihf":
optional: true
"@next/swc-linux-arm64-gnu":
optional: true
"@next/swc-linux-arm64-musl":
optional: true
"@next/swc-linux-x64-gnu":
optional: true
"@next/swc-linux-x64-musl":
optional: true
"@next/swc-win32-arm64-msvc":
optional: true
"@next/swc-win32-ia32-msvc":
optional: true
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
fibers:
optional: true
node-sass:
optional: true
sass:
optional: true
bin:
next: dist/bin/next
checksum: d96fc4f5bcd5a630d74111519f4820dcbd75dddf16c6d00d2167bd3cb8d74965d46d83c8e5ec301bf999013c7d96f1bfff9424f0221317d68b594c4d01f5825e
languageName: node
linkType: hard
"next@npm:^13.2.1":
version: 13.2.3
resolution: "next@npm:13.2.3"
@@ -29990,13 +30293,6 @@ __metadata:
languageName: node
linkType: hard
"oauth4webapi@npm:2.0.5":
version: 2.0.5
resolution: "oauth4webapi@npm:2.0.5"
checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9
languageName: node
linkType: hard
"oauth@npm:^0.9.15":
version: 0.9.15
resolution: "oauth@npm:0.9.15"
@@ -31303,23 +31599,23 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.32.1":
version: 1.32.1
resolution: "playwright-core@npm:1.32.1"
"playwright-core@npm:1.32.2":
version: 1.32.2
resolution: "playwright-core@npm:1.32.2"
bin:
playwright: cli.js
checksum: 796185e605df3c970885845c1fbf2de53db5d089d37946850483797c91b81ab9dc87c3440695e96a8c92613324ef3deaea698afaf2c379548a23f8ce074931de
checksum: ff000cbf280e5d558fe70fd3edf14910a2e86ec68b04e28327176268345be7b3f88a5d22d78e8dae677dd633dce6cd493237df199773b55312f2ae1ab85d711f
languageName: node
linkType: hard
"playwright@npm:^1.31.2":
version: 1.32.1
resolution: "playwright@npm:1.32.1"
version: 1.32.2
resolution: "playwright@npm:1.32.2"
dependencies:
playwright-core: 1.32.1
playwright-core: 1.32.2
bin:
playwright: cli.js
checksum: bb2cef8812abf60cf07bbf6edb4ac480ccbf0b129e001fd4f2d499c8888918bc87dcc0f346b5954e6b67cd67528c8263593b091f3c83609a91c8a276196c8951
checksum: 36967299a5c4c02830bcb7fb94b96d6c6a7a2d7e749feb924509e89b1fdc34f8ee2218cfaf75e5f32572663568b468ad28fdc170bced41869d5ed69f9a7ff384
languageName: node
linkType: hard
@@ -31652,17 +31948,6 @@ __metadata:
languageName: node
linkType: hard
"preact-render-to-string@npm:5.2.3":
version: 5.2.3
resolution: "preact-render-to-string@npm:5.2.3"
dependencies:
pretty-format: ^3.8.0
peerDependencies:
preact: ">=10"
checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
languageName: node
linkType: hard
"preact-render-to-string@npm:^5.1.19":
version: 5.2.6
resolution: "preact-render-to-string@npm:5.2.6"
@@ -31674,13 +31959,6 @@ __metadata:
languageName: node
linkType: hard
"preact@npm:10.11.3, preact@npm:^10.6.3":
version: 10.11.3
resolution: "preact@npm:10.11.3"
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
languageName: node
linkType: hard
"preact@npm:10.4.1":
version: 10.4.1
resolution: "preact@npm:10.4.1"
@@ -31695,6 +31973,13 @@ __metadata:
languageName: node
linkType: hard
"preact@npm:^10.6.3":
version: 10.11.3
resolution: "preact@npm:10.11.3"
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
languageName: node
linkType: hard
"prelude-ls@npm:^1.2.1":
version: 1.2.1
resolution: "prelude-ls@npm:1.2.1"
@@ -36273,6 +36558,20 @@ __metadata:
languageName: node
linkType: hard
"styled-jsx@npm:5.0.7":
version: 5.0.7
resolution: "styled-jsx@npm:5.0.7"
peerDependencies:
react: ">= 16.8.0 || 17.x.x || ^18.0.0-0"
peerDependenciesMeta:
"@babel/core":
optional: true
babel-plugin-macros:
optional: true
checksum: 61959993915f4b1662a682dbbefb3512de9399cf6901969bcadd26ba5441d2b5ca5c1021b233bbd573da2541b41efb45d56c6f618dbc8d88a381ebc62461fefe
languageName: node
linkType: hard
"styled-jsx@npm:5.1.1":
version: 5.1.1
resolution: "styled-jsx@npm:5.1.1"
@@ -37956,6 +38255,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:^4.7.4":
version: 4.9.5
resolution: "typescript@npm:4.9.5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db
languageName: node
linkType: hard
"typescript@npm:^4.9.4":
version: 4.9.4
resolution: "typescript@npm:4.9.4"
@@ -37966,6 +38275,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@^4.7.4#~builtin<compat/typescript>":
version: 4.9.5
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=23ec76"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d
languageName: node
linkType: hard
"typescript@patch:typescript@^4.9.4#~builtin<compat/typescript>":
version: 4.9.4
resolution: "typescript@patch:typescript@npm%3A4.9.4#~builtin<compat/typescript>::version=4.9.4&hash=23ec76"
@@ -37977,16 +38296,16 @@ __metadata:
linkType: hard
"tzdata@npm:^1.0.30":
version: 1.0.37
resolution: "tzdata@npm:1.0.37"
checksum: 5263d22e257639f292e314c15942d147e82211b370cbeccd5e16025b0fdb06371fdb989f247c386db60b580b2fe12b39601a3c21d35e09a037d60a92323cb3cd
version: 1.0.38
resolution: "tzdata@npm:1.0.38"
checksum: 866abff1316bd1311057f5de9da702c78a27f3bba213aa6986e8e4d22d0d25b554dda2330e6d853e2c34566270bb327babe467ad9a5f069ad466f09571872800
languageName: node
linkType: hard
"ua-parser-js@npm:^1.0.33":
version: 1.0.34
resolution: "ua-parser-js@npm:1.0.34"
checksum: 78924548870c67b93b4bc6bd3d9736badfca8cc02ea01510fb0ed38e296a8c0713a6bec6226ed2383d5d521bab651a6fcaa75d421f04cafebca0f93250480fb3
version: 1.0.35
resolution: "ua-parser-js@npm:1.0.35"
checksum: 02370d38a0c8b586f2503d1c3bbba5cbc0b97d407282f9023201a99e4c03eae4357a2800fdf50cf80d73ec25c0b0cc5bfbaa03975b0add4043d6e4c86712c9c1
languageName: node
linkType: hard
@@ -40555,7 +40874,7 @@ __metadata:
languageName: node
linkType: hard
"zod@npm:^3.17.3":
"zod@npm:^3.17.3, zod@npm:^3.18.0":
version: 3.21.4
resolution: "zod@npm:3.21.4"
checksum: f185ba87342ff16f7a06686767c2b2a7af41110c7edf7c1974095d8db7a73792696bcb4a00853de0d2edeb34a5b2ea6a55871bc864227dace682a0a28de33e1f