Compare commits

...
8 Commits
Author SHA1 Message Date
Peer Richelsen 1a7911e9e2 Update package.json
release.yml / release (push) Canceled after 0s
2023-04-19 08:57:31 +02:00
Crowdin Bot cb09b5bc5f New Crowdin translations by Github Action 2023-04-19 06:47:15 +00:00
Efraín Rochín a99031bdd2 fix: [CAL-1490] Google Cal/Outlook not actively syncing/self hosted (#8305) 2023-04-19 08:46:17 +02:00
ada8a98362 chore: add database indexes (#8361)
Co-authored-by: Mythie <me@lucasjamessmith.me>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-04-18 16:50:42 -07:00
Yagiz Nizipli e176e81bae perf: improve bookings performance (#8362) 2023-04-18 16:48:53 -07:00
zomars b64dd66c2c Fixes bundle comments
https://github.com/hashicorp/nextjs-bundle-analysis/issues/52#issuecomment-1513913373
2023-04-18 16:40:06 -07:00
Crowdin Bot 7fd162601b New Crowdin translations by Github Action 2023-04-18 23:29:50 +00:00
c1f346c935 Cal 1099 availability section defined hour mismatch (#7416)
* CAL-1099 Add prop for availability in formMethods

* fix: show correct availabilty name

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* CAL - 1099 / fix 12 hr time format bug

* fix: remove useEffect and fix vertical tab item ui

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* Fixes merge conflicts

* Update index.tsx

* More conflict fixes

* Update packages/ui/components/navigation/tabs/VerticalTabs.tsx

* Type fixes

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: zomars <zomars@me.com>
2023-04-18 23:29:02 +00:00
13 changed files with 498 additions and 142 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
- name: Analyze bundle
run: |
cd apps/web
npx -p nextjs-bundle-analysis report
npx -p nextjs-bundle-analysis@0.5.0 report
- name: Upload bundle
uses: actions/upload-artifact@v2
@@ -76,7 +76,7 @@ jobs:
id: fc
with:
issue-number: ${{ github.event.number }}
body-includes: "<!-- __NEXTJS_BUNDLE -->"
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
- name: Create Comment
uses: peter-evans/create-or-update-comment@v1.4.4
@@ -1,21 +1,23 @@
import { SchedulingType } from "@prisma/client";
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]";
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
import dayjs from "@calcom/dayjs";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
import { NewScheduleButton } from "@calcom/features/schedules";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
type AvailabilityOption = {
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
export type AvailabilityOption = {
label: string;
value: number;
isDefault: boolean;
@@ -62,40 +64,12 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
);
};
const AvailabilitySelect = ({
className = "",
options,
value,
...props
}: {
className?: string;
name: string;
value: AvailabilityOption | undefined;
options: AvailabilityOption[];
isDisabled?: boolean;
onBlur: () => void;
onChange: (value: AvailabilityOption | null) => void;
}) => {
const { t } = useLocale();
return (
<Select
placeholder={t("select")}
options={options}
isDisabled={props.isDisabled}
isSearchable={false}
onChange={props.onChange}
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
defaultValue={value}
components={{ Option, SingleValue }}
isMulti={false}
/>
);
};
const format = (date: Date, hour12: boolean) =>
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
);
Intl.DateTimeFormat(undefined, {
hour: "numeric",
minute: "numeric",
hourCycle: hour12 ? "h12" : "h24",
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
const EventTypeScheduleDetails = ({
isManagedEventType,
@@ -188,61 +162,57 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
);
const { watch } = useFormContext<FormValues>();
const watchSchedule = watch("schedule");
const formMethods = useFormContext<FormValues>();
const [options, setOptions] = useState<AvailabilityOption[]>([]);
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
onSuccess: ({ schedules }) => {
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
if (!data?.schedules.length && !isLoading)
return (
<EmptyScreen
Icon={Clock}
headline={t("new_schedule_heading")}
description={t("new_schedule_description")}
buttonRaw={<NewScheduleButton fromEventType />}
border={false}
/>
);
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
if (isManagedEventType) {
options.push({
value: 0,
label: t("members_default_schedule"),
isDefault: false,
isManaged: false,
});
}
const schedules = data?.schedules || [];
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
isManaged: false,
}));
setOptions(options);
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
if (isManagedEventType) {
options.push({
value: 0,
label: t("members_default_schedule"),
isDefault: false,
isManaged: false,
});
}
const scheduleId = formMethods.getValues("schedule");
const value = options.find((option) =>
scheduleId
? option.value === scheduleId
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (
isChildrenManagedEventType &&
watchSchedule &&
!schedules.find((schedule) => schedule.id === watchSchedule)
) {
options.push({
value: watchSchedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
isDefault: false,
isManaged: false,
});
}
formMethods.setValue("availability", value);
},
});
const value = options.find((option) =>
watchSchedule
? option.value === watchSchedule
: isManagedEventType
? option.value === 0
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
);
const availabilityValue = formMethods.watch("availability");
return (
<div className="space-y-4">
@@ -251,27 +221,33 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
{t("availability")}
{shouldLockIndicator("availability")}
</label>
<Controller
name="schedule"
render={({ field }) => (
<>
<AvailabilitySelect
value={value}
options={options}
onBlur={field.onBlur}
isDisabled={shouldLockDisableProps("schedule").disabled}
name={field.name}
onChange={(selected) => {
field.onChange(selected?.value || null);
}}
/>
</>
)}
/>
{isLoading && <SelectSkeletonLoader />}
{!isLoading && (
<Controller
name="schedule"
render={({ field }) => {
return (
<Select
placeholder={t("select")}
options={options}
isSearchable={false}
onChange={(selected) => {
field.onChange(selected?.value || null);
if (selected?.value) formMethods.setValue("availability", selected);
}}
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
value={availabilityValue}
components={{ Option, SingleValue }}
isMulti={false}
/>
);
}}
/>
)}
</div>
{value?.value !== 0 ? (
{availabilityValue?.value !== 0 ? (
<EventTypeScheduleDetails
selectedScheduleValue={value}
selectedScheduleValue={availabilityValue}
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
/>
) : (
@@ -51,6 +51,7 @@ import {
} from "@calcom/ui/components/icon";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
type Props = {
children: React.ReactNode;
@@ -63,6 +64,7 @@ type Props = {
enabledWorkflowsNumber: number;
formMethods: UseFormReturn<FormValues>;
isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
};
function getNavigation(props: {
@@ -71,8 +73,10 @@ function getNavigation(props: {
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
}) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
@@ -128,6 +132,7 @@ function EventTypeSingleLayout({
enabledWorkflowsNumber,
isUpdateMutationLoading,
formMethods,
availability,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -171,6 +176,7 @@ function EventTypeSingleLayout({
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
availability,
});
navigation.splice(1, 0, {
name: "availability",
@@ -214,7 +220,7 @@ function EventTypeSingleLayout({
});
}
return navigation;
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team, availability]);
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
eventType.slug
@@ -374,6 +380,8 @@ function EventTypeSingleLayout({
tabs={EventTypeTabs}
sticky
linkProps={{ shallow: true }}
itemClassname="items-start"
iconClassName="md:mt-px"
/>
</div>
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.8.3",
"version": "2.8.4",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+11 -3
View File
@@ -1,3 +1,4 @@
import type { NextPageContext } from "next";
import type { DocumentContext, DocumentProps } from "next/document";
import Document, { Head, Html, Main, NextScript } from "next/document";
import { z } from "zod";
@@ -7,15 +8,22 @@ import { getDirFromLang } from "@calcom/lib/i18n";
import { csp } from "@lib/csp";
type Props = Record<string, unknown> & DocumentProps;
function setHeader(ctx: NextPageContext, name: string, value: string) {
try {
ctx.res?.setHeader(name, value);
} catch (e) {
// Getting "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client" when revalidate calendar chache
console.log(`Error setting header ${name}=${value} for ${ctx.asPath || "unknown asPath"}`, e);
}
}
class MyDocument extends Document<Props> {
static async getInitialProps(ctx: DocumentContext) {
const { nonce } = csp(ctx.req || null, ctx.res || null);
if (!process.env.CSP_POLICY) {
ctx.res?.setHeader("x-csp", "not-opted-in");
setHeader(ctx, "x-csp", "not-opted-in");
} else if (!ctx.res?.getHeader("x-csp")) {
// If x-csp not set by gSSP, then it's initialPropsOnly
ctx.res?.setHeader("x-csp", "initialPropsOnly");
setHeader(ctx, "x-csp", "initialPropsOnly");
}
const asPath = ctx.asPath || "";
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
+67 -11
View File
@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import type { PeriodType } from "@prisma/client";
import type { SchedulingType } from "@prisma/client";
import type { PeriodType, SchedulingType } from "@prisma/client";
import type { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useEffect, useState } from "react";
@@ -18,10 +17,10 @@ import { CAL_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import type { Prisma } from "@calcom/prisma/client";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
@@ -34,6 +33,7 @@ import PageWrapper from "@components/PageWrapper";
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
@@ -94,6 +94,7 @@ export type FormValues = {
children: ChildrenEventType[];
hosts: { userId: number; isFixed: boolean }[];
bookingFields: z.infer<typeof eventTypeBookingFields>;
availability?: AvailabilityOption;
};
export type CustomInputParsed = typeof customInputSchema._output;
@@ -262,6 +263,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}, [defaultValues]);
const appsMetadata = formMethods.getValues("metadata")?.apps;
const availability = formMethods.watch("availability");
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
let numberOfActiveApps = 0;
@@ -378,6 +380,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
enabledWorkflowsNumber={eventType.workflows.length}
eventType={eventType}
team={team}
availability={availability}
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
@@ -385,13 +388,66 @@ const EventTypePage = (props: EventTypeSetupProps) => {
<Form
form={formMethods}
id="event-type-form"
handleSubmit={async (values: FormValues) => {
if (!values.children.length) return handleSubmit(values);
const existingSlugEventTypes = values.children.filter((ch) =>
ch.owner.eventTypeSlugs.includes(slug)
);
if (!existingSlugEventTypes.length) return handleSubmit(values);
setSlugExistsChildrenDialogOpen(existingSlugEventTypes);
handleSubmit={async (values) => {
const {
periodDates,
periodCountCalendarDays,
beforeBufferTime,
afterBufferTime,
seatsPerTimeSlot,
seatsShowAttendees,
bookingLimits,
durationLimits,
recurringEvent,
locations,
metadata,
customInputs,
// We don't need to send send these values to the backend
// eslint-disable-next-line @typescript-eslint/no-unused-vars
seatsPerTimeSlotEnabled,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
minimumBookingNoticeInDurationType,
availability,
...input
} = values;
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
if (durationLimits) {
const isValid = validateIntervalLimitOrder(durationLimits);
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
}
if (metadata?.multipleDuration !== undefined) {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
}
updateMutation.mutate({
...input,
locations,
recurringEvent,
periodStartDate: periodDates.startDate,
periodEndDate: periodDates.endDate,
periodCountCalendarDays: periodCountCalendarDays === "1",
id: eventType.id,
beforeEventBuffer: beforeBufferTime,
afterEventBuffer: afterBufferTime,
bookingLimits,
durationLimits,
seatsPerTimeSlot,
seatsShowAttendees,
metadata,
customInputs,
});
}}>
<div ref={animationParentRef}>{tabMap[tabName]}</div>
</Form>
@@ -1150,11 +1150,13 @@
"current_username": "Nom d'utilisateur actuel",
"example_1": "Exemple 1",
"example_2": "Exemple 2",
"booking_question_identifier": "Identifiant de la question de réservation",
"company_size": "Taille de l'entreprise",
"what_help_needed": "Avez-vous besoin d'aide ?",
"variable_format": "Format des variables",
"webhook_subscriber_url_reserved": "Le lien de l'abonné webhook est déjà défini",
"custom_input_as_variable_info": "Ignorer tous les caractères spéciaux du libellé de champ supplémentaire (utiliser uniquement des lettres et des chiffres), utiliser des majuscules pour toutes les lettres et remplacer les espaces par des tirets bas.",
"using_booking_questions_as_variables": "Comment puis-je utiliser les questions de réservation comme variables ?",
"download_desktop_app": "Télécharger l'application de bureau",
"set_ping_link": "Définir le lien Ping",
"rate_limit_exceeded": "Limite dépassée",
@@ -1380,6 +1382,7 @@
"add_limit": "Ajouter une limite",
"team_name_required": "Nom d'équipe requis",
"show_attendees": "Partagez les informations des participants entre les invités",
"how_booking_questions_as_variables": "Comment utiliser les questions de réservation comme variables ?",
"format": "Format",
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
@@ -1394,6 +1397,7 @@
"billing_help_title": "Besoin d'autre chose ?",
"billing_help_description": "Si vous avez besoin d'aide pour la facturation, notre équipe d'assistance est là pour vous aider.",
"billing_help_cta": "Contacter l'assistance",
"ignore_special_characters_booking_questions": "Ignorez les caractères spéciaux dans l'identifiant de votre question de réservation, utilisez uniquement des lettres et des chiffres.",
"retry": "Réessayer",
"fetching_calendars_error": "Un problème est survenu lors de la récupération de vos calendriers. Veuillez <1>réessayer</1> ou contacter le service client.",
"calendar_connection_fail": "Échec de la connexion au calendrier",
@@ -1702,6 +1706,13 @@
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
"verify": "Vérifier",
"timezone_variable": "Fuseau horaire",
"timezone_info": "Le fuseau horaire du destinataire",
"event_end_time_variable": "Heure de fin de l'événement",
"event_end_time_info": "L'heure de fin de l'événement",
"cancel_url_variable": "Lien d'annulation",
"cancel_url_info": "Le lien d'annulation de la réservation",
"reschedule_url_variable": "Lien de replanification",
"reschedule_url_info": "Le lien de replanification de la réservation",
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
"select_all": "Tout sélectionner",
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
@@ -1766,6 +1777,7 @@
"collect_no_show_fee": "Percevoir les frais d'absence",
"no_show_fee_charged": "Frais d'absence facturés",
"insights": "Statistiques",
"testing_workflow_info_message": "Lors du test de ce workflow, sachez que les e-mails et les SMS ne peuvent être programmés qu'au moins 1 heure à l'avance",
"insights_no_data_found_for_filter": "Aucune donnée trouvée pour le filtre ou les dates sélectionnés.",
"acknowledge_booking_no_show_fee": "Je reconnais que si je ne participe pas à cet événement, des frais d'absence de {{amount, currency}} seront appliqués à ma carte.",
"card_details": "Détails de la carte",
@@ -111,7 +111,7 @@
"hidden_team_owner_message": "Bạn cần tài khoản Pro để sử dụng nhóm, bạn sẽ bị ẩn cho đến khi bạn thanh toán.",
"link_expires": "p.s. Link hết hạn trong vòng {{expiresIn}} tiếng.",
"upgrade_to_per_seat": "Nâng cấp từng tài khoản",
"seat_options_doesnt_support_confirmation": "Tuỳ chọn chỗ ngồi không hỗ trợ yêu cầu xác nhận",
"seat_options_doesnt_support_confirmation": "Tuỳ chọn chỗ ngồi không thể chọn cho yêu cầu xác nhận đặt lịch",
"team_upgrade_seats_details": "Trong tổng {{memberCount}} thành viên của nhóm bạn, {{unpaidCount}} thành viên chưa được thanh toán. Với {{seatPrice}}$/tháng mỗi thành viên, tổng chi phí cho nhóm bạn là {{totalCost}}$/tháng.",
"team_upgrade_banner_description": "Cám ơn bạn đã dùng thử gói kế hoạch mới dành cho nhóm. Chúng tôi để ý thấy nhóm \"{{teamName}}\" của bạn cần được nâng cấp.",
"team_upgrade_banner_action": "Nâng cấp tại đây",
@@ -0,0 +1,182 @@
-- CreateIndex
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
-- CreateIndex
CREATE INDEX "Account_type_idx" ON "Account"("type");
-- CreateIndex
CREATE INDEX "ApiKey_userId_idx" ON "ApiKey"("userId");
-- CreateIndex
CREATE INDEX "App_enabled_idx" ON "App"("enabled");
-- CreateIndex
CREATE INDEX "App_RoutingForms_Form_userId_idx" ON "App_RoutingForms_Form"("userId");
-- CreateIndex
CREATE INDEX "App_RoutingForms_Form_disabled_idx" ON "App_RoutingForms_Form"("disabled");
-- CreateIndex
CREATE INDEX "App_RoutingForms_FormResponse_formFillerId_idx" ON "App_RoutingForms_FormResponse"("formFillerId");
-- CreateIndex
CREATE INDEX "App_RoutingForms_FormResponse_formId_idx" ON "App_RoutingForms_FormResponse"("formId");
-- CreateIndex
CREATE INDEX "Attendee_email_idx" ON "Attendee"("email");
-- CreateIndex
CREATE INDEX "Attendee_bookingId_idx" ON "Attendee"("bookingId");
-- CreateIndex
CREATE INDEX "Availability_userId_idx" ON "Availability"("userId");
-- CreateIndex
CREATE INDEX "Booking_eventTypeId_idx" ON "Booking"("eventTypeId");
-- CreateIndex
CREATE INDEX "Booking_userId_idx" ON "Booking"("userId");
-- CreateIndex
CREATE INDEX "Booking_destinationCalendarId_idx" ON "Booking"("destinationCalendarId");
-- CreateIndex
CREATE INDEX "Booking_recurringEventId_idx" ON "Booking"("recurringEventId");
-- CreateIndex
CREATE INDEX "Booking_uid_idx" ON "Booking"("uid");
-- CreateIndex
CREATE INDEX "Booking_status_idx" ON "Booking"("status");
-- CreateIndex
CREATE INDEX "BookingReference_bookingId_idx" ON "BookingReference"("bookingId");
-- CreateIndex
CREATE INDEX "BookingReference_credentialId_idx" ON "BookingReference"("credentialId");
-- CreateIndex
CREATE INDEX "BookingReference_type_idx" ON "BookingReference"("type");
-- CreateIndex
CREATE INDEX "BookingReference_uid_idx" ON "BookingReference"("uid");
-- CreateIndex
CREATE INDEX "BookingSeat_bookingId_idx" ON "BookingSeat"("bookingId");
-- CreateIndex
CREATE INDEX "BookingSeat_attendeeId_idx" ON "BookingSeat"("attendeeId");
-- CreateIndex
CREATE INDEX "Credential_userId_idx" ON "Credential"("userId");
-- CreateIndex
CREATE INDEX "Credential_appId_idx" ON "Credential"("appId");
-- CreateIndex
CREATE INDEX "DestinationCalendar_userId_idx" ON "DestinationCalendar"("userId");
-- CreateIndex
CREATE INDEX "DestinationCalendar_eventTypeId_idx" ON "DestinationCalendar"("eventTypeId");
-- CreateIndex
CREATE INDEX "DestinationCalendar_credentialId_idx" ON "DestinationCalendar"("credentialId");
-- CreateIndex
CREATE INDEX "EventType_userId_idx" ON "EventType"("userId");
-- CreateIndex
CREATE INDEX "EventType_teamId_idx" ON "EventType"("teamId");
-- CreateIndex
CREATE INDEX "EventTypeCustomInput_eventTypeId_idx" ON "EventTypeCustomInput"("eventTypeId");
-- CreateIndex
CREATE INDEX "Feature_enabled_idx" ON "Feature"("enabled");
-- CreateIndex
CREATE INDEX "Feature_stale_idx" ON "Feature"("stale");
-- CreateIndex
CREATE INDEX "Feedback_userId_idx" ON "Feedback"("userId");
-- CreateIndex
CREATE INDEX "Feedback_rating_idx" ON "Feedback"("rating");
-- CreateIndex
CREATE INDEX "Host_userId_idx" ON "Host"("userId");
-- CreateIndex
CREATE INDEX "Host_eventTypeId_idx" ON "Host"("eventTypeId");
-- CreateIndex
CREATE INDEX "Membership_teamId_idx" ON "Membership"("teamId");
-- CreateIndex
CREATE INDEX "Membership_userId_idx" ON "Membership"("userId");
-- CreateIndex
CREATE INDEX "Payment_bookingId_idx" ON "Payment"("bookingId");
-- CreateIndex
CREATE INDEX "Payment_externalId_idx" ON "Payment"("externalId");
-- CreateIndex
CREATE INDEX "ReminderMail_referenceId_idx" ON "ReminderMail"("referenceId");
-- CreateIndex
CREATE INDEX "ReminderMail_reminderType_idx" ON "ReminderMail"("reminderType");
-- CreateIndex
CREATE INDEX "SelectedCalendar_userId_idx" ON "SelectedCalendar"("userId");
-- CreateIndex
CREATE INDEX "SelectedCalendar_integration_idx" ON "SelectedCalendar"("integration");
-- CreateIndex
CREATE INDEX "SelectedCalendar_externalId_idx" ON "SelectedCalendar"("externalId");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE INDEX "VerificationToken_token_idx" ON "VerificationToken"("token");
-- CreateIndex
CREATE INDEX "VerifiedNumber_userId_idx" ON "VerifiedNumber"("userId");
-- CreateIndex
CREATE INDEX "VerifiedNumber_teamId_idx" ON "VerifiedNumber"("teamId");
-- CreateIndex
CREATE INDEX "Workflow_userId_idx" ON "Workflow"("userId");
-- CreateIndex
CREATE INDEX "Workflow_teamId_idx" ON "Workflow"("teamId");
-- CreateIndex
CREATE INDEX "WorkflowReminder_bookingUid_idx" ON "WorkflowReminder"("bookingUid");
-- CreateIndex
CREATE INDEX "WorkflowReminder_workflowStepId_idx" ON "WorkflowReminder"("workflowStepId");
-- CreateIndex
CREATE INDEX "WorkflowStep_workflowId_idx" ON "WorkflowStep"("workflowId");
-- CreateIndex
CREATE INDEX "WorkflowsOnEventTypes_workflowId_idx" ON "WorkflowsOnEventTypes"("workflowId");
-- CreateIndex
CREATE INDEX "WorkflowsOnEventTypes_eventTypeId_idx" ON "WorkflowsOnEventTypes"("eventTypeId");
-- CreateIndex
CREATE INDEX "users_email_idx" ON "users"("email");
-- CreateIndex
CREATE INDEX "users_emailVerified_idx" ON "users"("emailVerified");
-- CreateIndex
CREATE INDEX "users_identityProvider_idx" ON "users"("identityProvider");
-- CreateIndex
CREATE INDEX "users_identityProviderId_idx" ON "users"("identityProviderId");
+81
View File
@@ -38,6 +38,8 @@ model Host {
isFixed Boolean @default(false)
@@id([userId, eventTypeId])
@@index([userId])
@@index([eventTypeId])
}
model EventType {
@@ -108,6 +110,8 @@ model EventType {
@@unique([userId, slug])
@@unique([teamId, slug])
@@unique([userId, parentId])
@@index([userId])
@@index([teamId])
}
model Credential {
@@ -122,6 +126,9 @@ model Credential {
appId String?
destinationCalendars DestinationCalendar[]
invalid Boolean? @default(false)
@@index([userId])
@@index([appId])
}
enum IdentityProvider {
@@ -141,6 +148,10 @@ model DestinationCalendar {
eventTypeId Int? @unique
credentialId Int?
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([eventTypeId])
@@index([credentialId])
}
enum UserPermissionRole {
@@ -210,6 +221,10 @@ model User {
verifiedNumbers VerifiedNumber[]
hosts Host[]
@@index([email])
@@index([emailVerified])
@@index([identityProvider])
@@index([identityProviderId])
@@map(name: "users")
}
@@ -251,6 +266,8 @@ model Membership {
disableImpersonation Boolean @default(false)
@@id([userId, teamId])
@@index([teamId])
@@index([userId])
}
model VerificationToken {
@@ -262,6 +279,7 @@ model VerificationToken {
updatedAt DateTime @updatedAt
@@unique([identifier, token])
@@index([token])
}
model BookingReference {
@@ -278,6 +296,11 @@ model BookingReference {
externalCalendarId String?
deleted Boolean?
credentialId Int?
@@index([bookingId])
@@index([credentialId])
@@index([type])
@@index([uid])
}
model Attendee {
@@ -289,6 +312,9 @@ model Attendee {
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
bookingId Int?
bookingSeat BookingSeat?
@@index([email])
@@index([bookingId])
}
enum BookingStatus {
@@ -336,6 +362,13 @@ model Booking {
/// @zod.custom(imports.bookingMetadataSchema)
metadata Json?
isRecorded Boolean @default(false)
@@index([eventTypeId])
@@index([userId])
@@index([destinationCalendarId])
@@index([recurringEventId])
@@index([uid])
@@index([status])
}
model Schedule {
@@ -363,6 +396,7 @@ model Availability {
Schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
@@index([userId])
@@index([eventTypeId])
@@index([scheduleId])
}
@@ -374,6 +408,9 @@ model SelectedCalendar {
externalId String
@@id([userId, integration, externalId])
@@index([userId])
@@index([integration])
@@index([externalId])
}
enum EventTypeCustomInputType {
@@ -395,6 +432,8 @@ model EventTypeCustomInput {
options Json?
required Boolean
placeholder String @default("")
@@index([eventTypeId])
}
model ResetPasswordRequest {
@@ -415,6 +454,9 @@ model ReminderMail {
reminderType ReminderType
elapsedMinutes Int
createdAt DateTime @default(now())
@@index([referenceId])
@@index([reminderType])
}
model Payment {
@@ -432,6 +474,9 @@ model Payment {
data Json
externalId String @unique
paymentOption PaymentOption? @default(ON_BOOKING)
@@index([bookingId])
@@index([externalId])
}
enum PaymentOption {
@@ -486,6 +531,8 @@ model ApiKey {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
appId String?
@@index([userId])
}
model HashedLink {
@@ -512,6 +559,8 @@ model Account {
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@index([userId])
@@index([type])
}
model Session {
@@ -520,6 +569,8 @@ model Session {
userId Int
expires DateTime
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
enum AppCategories {
@@ -549,6 +600,8 @@ model App {
Webhook Webhook[]
ApiKey ApiKey[]
enabled Boolean @default(false)
@@index([enabled])
}
model App_RoutingForms_Form {
@@ -565,6 +618,9 @@ model App_RoutingForms_Form {
disabled Boolean @default(false)
/// @zod.custom(imports.RoutingFormSettings)
settings Json?
@@index([userId])
@@index([disabled])
}
model App_RoutingForms_FormResponse {
@@ -576,6 +632,8 @@ model App_RoutingForms_FormResponse {
createdAt DateTime @default(now())
@@unique([formFillerId, formId])
@@index([formFillerId])
@@index([formId])
}
model Feedback {
@@ -585,6 +643,9 @@ model Feedback {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
rating String
comment String?
@@index([userId])
@@index([rating])
}
enum WorkflowTriggerEvents {
@@ -617,6 +678,8 @@ model WorkflowStep {
numberRequired Boolean?
sender String?
numberVerificationPending Boolean @default(true)
@@index([workflowId])
}
model Workflow {
@@ -631,6 +694,9 @@ model Workflow {
time Int?
timeUnit TimeUnit?
steps WorkflowStep[]
@@index([userId])
@@index([teamId])
}
model WorkflowsOnEventTypes {
@@ -639,6 +705,9 @@ model WorkflowsOnEventTypes {
workflowId Int
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
@@index([workflowId])
@@index([eventTypeId])
}
model Deployment {
@@ -668,6 +737,9 @@ model WorkflowReminder {
workflowStepId Int?
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
cancelled Boolean?
@@index([bookingUid])
@@index([workflowStepId])
}
enum WorkflowTemplates {
@@ -688,6 +760,9 @@ model BookingSeat {
attendeeId Int @unique
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
data Json?
@@index([bookingId])
@@index([attendeeId])
}
model VerifiedNumber {
@@ -697,6 +772,9 @@ model VerifiedNumber {
teamId Int?
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
phoneNumber String
@@index([userId])
@@index([teamId])
}
model Feature {
@@ -714,6 +792,9 @@ model Feature {
createdAt DateTime? @default(now())
updatedAt DateTime? @default(now()) @updatedAt
updatedBy Int?
@@index([enabled])
@@index([stale])
}
enum FeatureType {
@@ -780,10 +780,11 @@ export const bookingsRouter = router({
},
});
const authorized = async () => {
if (booking.userId !== user.id && booking.eventTypeId) {
// Only query database when it is explicitly required.
const eventType = await prisma.eventType.findFirst({
where: {
id: booking.eventTypeId || undefined,
id: booking.eventTypeId,
schedulingType: SchedulingType.COLLECTIVE,
},
select: {
@@ -791,15 +792,16 @@ export const bookingsRouter = router({
},
});
return eventType && eventType.users.find((user) => booking.userId === user.id);
};
if (booking.userId !== user.id && !(await authorized())) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
if (eventType && !eventType.users.find((user) => booking.userId === user.id)) {
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
}
}
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
// Do not move this before authorization check.
// This is done to avoid exposing extra information to the requester.
if (booking.status === BookingStatus.ACCEPTED) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
}
// If booking requires payment and is not paid, we don't allow confirmation
if (confirmed && booking.payment.length > 0 && !booking.paid) {
@@ -815,14 +817,22 @@ export const bookingsRouter = router({
return { message: "Booking confirmed", status: BookingStatus.ACCEPTED };
}
// Cache translations to avoid requesting multiple times.
const translations = new Map();
const attendeesListPromises = booking.attendees.map(async (attendee) => {
const locale = attendee.locale ?? "en";
let translate = translations.get(locale);
if (!translate) {
translate = await getTranslation(locale, "common");
translations.set(locale, translate);
}
return {
name: attendee.name,
email: attendee.email,
timeZone: attendee.timeZone,
language: {
translate: await getTranslation(attendee.locale ?? "en", "common"),
locale: attendee.locale ?? "en",
translate,
locale,
},
};
});
@@ -864,6 +874,9 @@ export const bookingsRouter = router({
recurringEventId,
id: booking.id,
},
select: {
id: true,
},
}))
) {
// FIXME: It might be best to retrieve recurringEventId from the booking itself.
@@ -25,9 +25,10 @@ export type VerticalTabItemProps = {
isExternalLink?: boolean;
linkProps?: Omit<ComponentProps<typeof Link>, "href">;
avatar?: string;
iconClassName?: string;
};
const VerticalTabItem = function ({
const VerticalTabItem = ({
name,
href,
info,
@@ -35,11 +36,10 @@ const VerticalTabItem = function ({
disableChevron,
linkProps,
...props
}: VerticalTabItemProps) {
}: VerticalTabItemProps) => {
const { t } = useLocale();
const { asPath } = useRouter();
const isCurrent = asPath.startsWith(href);
return (
<Fragment key={name}>
{!props.hidden && (
@@ -60,7 +60,12 @@ const VerticalTabItem = function ({
data-testid={`vertical-tab-${name}`}
aria-current={isCurrent ? "page" : undefined}>
{props.icon && (
<props.icon className="h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0" />
<props.icon
className={classNames(
"h-[16px] w-[16px] stroke-[2px] ltr:mr-2 rtl:ml-2 md:mt-0",
props.iconClassName
)}
/>
)}
<div className="h-fit">
<span className="flex items-center space-x-2 rtl:space-x-reverse">
@@ -12,9 +12,18 @@ export interface NavTabProps {
sticky?: boolean;
linkProps?: VerticalTabItemProps["linkProps"];
itemClassname?: string;
iconClassName?: string;
}
const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassname, ...props }: NavTabProps) {
const NavTabs = function ({
tabs,
className = "",
sticky,
linkProps,
itemClassname,
iconClassName,
...props
}: NavTabProps) {
return (
<nav
className={classNames(
@@ -27,7 +36,13 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassna
{sticky && <div className="pt-6" />}
{props.children}
{tabs.map((tab, idx) => (
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={itemClassname} />
<VerticalTabItem
{...tab}
key={idx}
linkProps={linkProps}
className={itemClassname}
iconClassName={iconClassName}
/>
))}
</nav>
);