Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a7911e9e2 | ||
|
|
cb09b5bc5f | ||
|
|
a99031bdd2 | ||
|
|
ada8a98362 | ||
|
|
e176e81bae | ||
|
|
b64dd66c2c | ||
|
|
7fd162601b | ||
|
|
c1f346c935 |
No files matched your search
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Analyze bundle
|
- name: Analyze bundle
|
||||||
run: |
|
run: |
|
||||||
cd apps/web
|
cd apps/web
|
||||||
npx -p nextjs-bundle-analysis report
|
npx -p nextjs-bundle-analysis@0.5.0 report
|
||||||
|
|
||||||
- name: Upload bundle
|
- name: Upload bundle
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
@@ -76,7 +76,7 @@ jobs:
|
|||||||
id: fc
|
id: fc
|
||||||
with:
|
with:
|
||||||
issue-number: ${{ github.event.number }}
|
issue-number: ${{ github.event.number }}
|
||||||
body-includes: "<!-- __NEXTJS_BUNDLE -->"
|
body-includes: "<!-- __NEXTJS_BUNDLE_@calcom/web -->"
|
||||||
|
|
||||||
- name: Create Comment
|
- name: Create Comment
|
||||||
uses: peter-evans/create-or-update-comment@v1.4.4
|
uses: peter-evans/create-or-update-comment@v1.4.4
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
import { SchedulingType } from "@prisma/client";
|
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 { Controller, useFormContext } from "react-hook-form";
|
||||||
import type { OptionProps, SingleValueProps } from "react-select";
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||||
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
|
import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type AvailabilityOption = {
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
|
export type AvailabilityOption = {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
isDefault: boolean;
|
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) =>
|
const format = (date: Date, hour12: boolean) =>
|
||||||
Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "numeric", hour12 }).format(
|
Intl.DateTimeFormat(undefined, {
|
||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
hour: "numeric",
|
||||||
);
|
minute: "numeric",
|
||||||
|
hourCycle: hour12 ? "h12" : "h24",
|
||||||
|
}).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
|
||||||
|
|
||||||
const EventTypeScheduleDetails = ({
|
const EventTypeScheduleDetails = ({
|
||||||
isManagedEventType,
|
isManagedEventType,
|
||||||
@@ -188,61 +162,57 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
|||||||
);
|
);
|
||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
const watchSchedule = watch("schedule");
|
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)
|
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||||
return (
|
if (isManagedEventType) {
|
||||||
<EmptyScreen
|
options.push({
|
||||||
Icon={Clock}
|
value: 0,
|
||||||
headline={t("new_schedule_heading")}
|
label: t("members_default_schedule"),
|
||||||
description={t("new_schedule_description")}
|
isDefault: false,
|
||||||
buttonRaw={<NewScheduleButton fromEventType />}
|
isManaged: false,
|
||||||
border={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) => ({
|
setOptions(options);
|
||||||
value: schedule.id,
|
|
||||||
label: schedule.name,
|
|
||||||
isDefault: schedule.isDefault,
|
|
||||||
isManaged: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
const scheduleId = formMethods.getValues("schedule");
|
||||||
if (isManagedEventType) {
|
const value = options.find((option) =>
|
||||||
options.push({
|
scheduleId
|
||||||
value: 0,
|
? option.value === scheduleId
|
||||||
label: t("members_default_schedule"),
|
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||||
isDefault: false,
|
);
|
||||||
isManaged: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We are showing a managed event for a member and team owner selected their own schedule, so adding
|
formMethods.setValue("availability", value);
|
||||||
// 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 value = options.find((option) =>
|
const availabilityValue = formMethods.watch("availability");
|
||||||
watchSchedule
|
|
||||||
? option.value === watchSchedule
|
|
||||||
: isManagedEventType
|
|
||||||
? option.value === 0
|
|
||||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -251,27 +221,33 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
|||||||
{t("availability")}
|
{t("availability")}
|
||||||
{shouldLockIndicator("availability")}
|
{shouldLockIndicator("availability")}
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
{isLoading && <SelectSkeletonLoader />}
|
||||||
name="schedule"
|
{!isLoading && (
|
||||||
render={({ field }) => (
|
<Controller
|
||||||
<>
|
name="schedule"
|
||||||
<AvailabilitySelect
|
render={({ field }) => {
|
||||||
value={value}
|
return (
|
||||||
options={options}
|
<Select
|
||||||
onBlur={field.onBlur}
|
placeholder={t("select")}
|
||||||
isDisabled={shouldLockDisableProps("schedule").disabled}
|
options={options}
|
||||||
name={field.name}
|
isSearchable={false}
|
||||||
onChange={(selected) => {
|
onChange={(selected) => {
|
||||||
field.onChange(selected?.value || null);
|
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>
|
</div>
|
||||||
{value?.value !== 0 ? (
|
{availabilityValue?.value !== 0 ? (
|
||||||
<EventTypeScheduleDetails
|
<EventTypeScheduleDetails
|
||||||
selectedScheduleValue={value}
|
selectedScheduleValue={availabilityValue}
|
||||||
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import {
|
|||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -63,6 +64,7 @@ type Props = {
|
|||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
formMethods: UseFormReturn<FormValues>;
|
formMethods: UseFormReturn<FormValues>;
|
||||||
isUpdateMutationLoading?: boolean;
|
isUpdateMutationLoading?: boolean;
|
||||||
|
availability?: AvailabilityOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNavigation(props: {
|
function getNavigation(props: {
|
||||||
@@ -71,8 +73,10 @@ function getNavigation(props: {
|
|||||||
enabledAppsNumber: number;
|
enabledAppsNumber: number;
|
||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
}) {
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber, availability } =
|
||||||
|
props;
|
||||||
const duration =
|
const duration =
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
||||||
|
|
||||||
@@ -128,6 +132,7 @@ function EventTypeSingleLayout({
|
|||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
isUpdateMutationLoading,
|
isUpdateMutationLoading,
|
||||||
formMethods,
|
formMethods,
|
||||||
|
availability,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -171,6 +176,7 @@ function EventTypeSingleLayout({
|
|||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
|
availability,
|
||||||
});
|
});
|
||||||
navigation.splice(1, 0, {
|
navigation.splice(1, 0, {
|
||||||
name: "availability",
|
name: "availability",
|
||||||
@@ -214,7 +220,7 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
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}/${
|
const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
|
||||||
eventType.slug
|
eventType.slug
|
||||||
@@ -374,6 +380,8 @@ function EventTypeSingleLayout({
|
|||||||
tabs={EventTypeTabs}
|
tabs={EventTypeTabs}
|
||||||
sticky
|
sticky
|
||||||
linkProps={{ shallow: true }}
|
linkProps={{ shallow: true }}
|
||||||
|
itemClassname="items-start"
|
||||||
|
iconClassName="md:mt-px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
<div className="p-2 md:mx-0 md:p-0 xl:hidden">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.8.3",
|
"version": "2.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { NextPageContext } from "next";
|
||||||
import type { DocumentContext, DocumentProps } from "next/document";
|
import type { DocumentContext, DocumentProps } from "next/document";
|
||||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -7,15 +8,22 @@ import { getDirFromLang } from "@calcom/lib/i18n";
|
|||||||
import { csp } from "@lib/csp";
|
import { csp } from "@lib/csp";
|
||||||
|
|
||||||
type Props = Record<string, unknown> & DocumentProps;
|
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> {
|
class MyDocument extends Document<Props> {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
const { nonce } = csp(ctx.req || null, ctx.res || null);
|
const { nonce } = csp(ctx.req || null, ctx.res || null);
|
||||||
if (!process.env.CSP_POLICY) {
|
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")) {
|
} else if (!ctx.res?.getHeader("x-csp")) {
|
||||||
// If x-csp not set by gSSP, then it's initialPropsOnly
|
// 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 || "";
|
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
|
// Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import type { PeriodType } from "@prisma/client";
|
import type { PeriodType, SchedulingType } from "@prisma/client";
|
||||||
import type { SchedulingType } from "@prisma/client";
|
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -18,10 +17,10 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
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 type { Prisma } from "@calcom/prisma/client";
|
||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
|
||||||
import type { customInputSchema, EventTypeMetaDataSchema } 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 type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
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
|
// 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 { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
|
||||||
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
||||||
|
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||||
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
|
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
|
||||||
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
||||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||||
@@ -94,6 +94,7 @@ export type FormValues = {
|
|||||||
children: ChildrenEventType[];
|
children: ChildrenEventType[];
|
||||||
hosts: { userId: number; isFixed: boolean }[];
|
hosts: { userId: number; isFixed: boolean }[];
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||||
|
availability?: AvailabilityOption;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomInputParsed = typeof customInputSchema._output;
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
@@ -262,6 +263,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
}, [defaultValues]);
|
}, [defaultValues]);
|
||||||
|
|
||||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||||
|
const availability = formMethods.watch("availability");
|
||||||
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
|
const numberOfInstalledApps = eventTypeApps?.filter((app) => app.isInstalled).length || 0;
|
||||||
let numberOfActiveApps = 0;
|
let numberOfActiveApps = 0;
|
||||||
|
|
||||||
@@ -378,6 +380,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
enabledWorkflowsNumber={eventType.workflows.length}
|
enabledWorkflowsNumber={eventType.workflows.length}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
team={team}
|
team={team}
|
||||||
|
availability={availability}
|
||||||
isUpdateMutationLoading={updateMutation.isLoading}
|
isUpdateMutationLoading={updateMutation.isLoading}
|
||||||
formMethods={formMethods}
|
formMethods={formMethods}
|
||||||
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||||
@@ -385,13 +388,66 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
id="event-type-form"
|
id="event-type-form"
|
||||||
handleSubmit={async (values: FormValues) => {
|
handleSubmit={async (values) => {
|
||||||
if (!values.children.length) return handleSubmit(values);
|
const {
|
||||||
const existingSlugEventTypes = values.children.filter((ch) =>
|
periodDates,
|
||||||
ch.owner.eventTypeSlugs.includes(slug)
|
periodCountCalendarDays,
|
||||||
);
|
beforeBufferTime,
|
||||||
if (!existingSlugEventTypes.length) return handleSubmit(values);
|
afterBufferTime,
|
||||||
setSlugExistsChildrenDialogOpen(existingSlugEventTypes);
|
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>
|
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -1150,11 +1150,13 @@
|
|||||||
"current_username": "Nom d'utilisateur actuel",
|
"current_username": "Nom d'utilisateur actuel",
|
||||||
"example_1": "Exemple 1",
|
"example_1": "Exemple 1",
|
||||||
"example_2": "Exemple 2",
|
"example_2": "Exemple 2",
|
||||||
|
"booking_question_identifier": "Identifiant de la question de réservation",
|
||||||
"company_size": "Taille de l'entreprise",
|
"company_size": "Taille de l'entreprise",
|
||||||
"what_help_needed": "Avez-vous besoin d'aide ?",
|
"what_help_needed": "Avez-vous besoin d'aide ?",
|
||||||
"variable_format": "Format des variables",
|
"variable_format": "Format des variables",
|
||||||
"webhook_subscriber_url_reserved": "Le lien de l'abonné webhook est déjà défini",
|
"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.",
|
"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",
|
"download_desktop_app": "Télécharger l'application de bureau",
|
||||||
"set_ping_link": "Définir le lien Ping",
|
"set_ping_link": "Définir le lien Ping",
|
||||||
"rate_limit_exceeded": "Limite dépassée",
|
"rate_limit_exceeded": "Limite dépassée",
|
||||||
@@ -1380,6 +1382,7 @@
|
|||||||
"add_limit": "Ajouter une limite",
|
"add_limit": "Ajouter une limite",
|
||||||
"team_name_required": "Nom d'équipe requis",
|
"team_name_required": "Nom d'équipe requis",
|
||||||
"show_attendees": "Partagez les informations des participants entre les invités",
|
"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",
|
"format": "Format",
|
||||||
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
|
"uppercase_for_letters": "Utilisez des majuscules pour toutes les lettres.",
|
||||||
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
|
"replace_whitespaces_underscores": "Remplacez les espaces par des tirets bas.",
|
||||||
@@ -1394,6 +1397,7 @@
|
|||||||
"billing_help_title": "Besoin d'autre chose ?",
|
"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_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",
|
"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",
|
"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.",
|
"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",
|
"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 ?",
|
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
||||||
"verify": "Vérifier",
|
"verify": "Vérifier",
|
||||||
"timezone_variable": "Fuseau horaire",
|
"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",
|
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
|
||||||
"select_all": "Tout sélectionner",
|
"select_all": "Tout sélectionner",
|
||||||
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
|
"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",
|
"collect_no_show_fee": "Percevoir les frais d'absence",
|
||||||
"no_show_fee_charged": "Frais d'absence facturés",
|
"no_show_fee_charged": "Frais d'absence facturés",
|
||||||
"insights": "Statistiques",
|
"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.",
|
"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.",
|
"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",
|
"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.",
|
"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.",
|
"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",
|
"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_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_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",
|
"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");
|
||||||
@@ -38,6 +38,8 @@ model Host {
|
|||||||
isFixed Boolean @default(false)
|
isFixed Boolean @default(false)
|
||||||
|
|
||||||
@@id([userId, eventTypeId])
|
@@id([userId, eventTypeId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([eventTypeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model EventType {
|
model EventType {
|
||||||
@@ -108,6 +110,8 @@ model EventType {
|
|||||||
@@unique([userId, slug])
|
@@unique([userId, slug])
|
||||||
@@unique([teamId, slug])
|
@@unique([teamId, slug])
|
||||||
@@unique([userId, parentId])
|
@@unique([userId, parentId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Credential {
|
model Credential {
|
||||||
@@ -122,6 +126,9 @@ model Credential {
|
|||||||
appId String?
|
appId String?
|
||||||
destinationCalendars DestinationCalendar[]
|
destinationCalendars DestinationCalendar[]
|
||||||
invalid Boolean? @default(false)
|
invalid Boolean? @default(false)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([appId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum IdentityProvider {
|
enum IdentityProvider {
|
||||||
@@ -141,6 +148,10 @@ model DestinationCalendar {
|
|||||||
eventTypeId Int? @unique
|
eventTypeId Int? @unique
|
||||||
credentialId Int?
|
credentialId Int?
|
||||||
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
|
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([eventTypeId])
|
||||||
|
@@index([credentialId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum UserPermissionRole {
|
enum UserPermissionRole {
|
||||||
@@ -210,6 +221,10 @@ model User {
|
|||||||
verifiedNumbers VerifiedNumber[]
|
verifiedNumbers VerifiedNumber[]
|
||||||
hosts Host[]
|
hosts Host[]
|
||||||
|
|
||||||
|
@@index([email])
|
||||||
|
@@index([emailVerified])
|
||||||
|
@@index([identityProvider])
|
||||||
|
@@index([identityProviderId])
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +266,8 @@ model Membership {
|
|||||||
disableImpersonation Boolean @default(false)
|
disableImpersonation Boolean @default(false)
|
||||||
|
|
||||||
@@id([userId, teamId])
|
@@id([userId, teamId])
|
||||||
|
@@index([teamId])
|
||||||
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
@@ -262,6 +279,7 @@ model VerificationToken {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@unique([identifier, token])
|
@@unique([identifier, token])
|
||||||
|
@@index([token])
|
||||||
}
|
}
|
||||||
|
|
||||||
model BookingReference {
|
model BookingReference {
|
||||||
@@ -278,6 +296,11 @@ model BookingReference {
|
|||||||
externalCalendarId String?
|
externalCalendarId String?
|
||||||
deleted Boolean?
|
deleted Boolean?
|
||||||
credentialId Int?
|
credentialId Int?
|
||||||
|
|
||||||
|
@@index([bookingId])
|
||||||
|
@@index([credentialId])
|
||||||
|
@@index([type])
|
||||||
|
@@index([uid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Attendee {
|
model Attendee {
|
||||||
@@ -289,6 +312,9 @@ model Attendee {
|
|||||||
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
booking Booking? @relation(fields: [bookingId], references: [id], onDelete: Cascade)
|
||||||
bookingId Int?
|
bookingId Int?
|
||||||
bookingSeat BookingSeat?
|
bookingSeat BookingSeat?
|
||||||
|
|
||||||
|
@@index([email])
|
||||||
|
@@index([bookingId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BookingStatus {
|
enum BookingStatus {
|
||||||
@@ -336,6 +362,13 @@ model Booking {
|
|||||||
/// @zod.custom(imports.bookingMetadataSchema)
|
/// @zod.custom(imports.bookingMetadataSchema)
|
||||||
metadata Json?
|
metadata Json?
|
||||||
isRecorded Boolean @default(false)
|
isRecorded Boolean @default(false)
|
||||||
|
|
||||||
|
@@index([eventTypeId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([destinationCalendarId])
|
||||||
|
@@index([recurringEventId])
|
||||||
|
@@index([uid])
|
||||||
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Schedule {
|
model Schedule {
|
||||||
@@ -363,6 +396,7 @@ model Availability {
|
|||||||
Schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
Schedule Schedule? @relation(fields: [scheduleId], references: [id])
|
||||||
scheduleId Int?
|
scheduleId Int?
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
@@index([eventTypeId])
|
@@index([eventTypeId])
|
||||||
@@index([scheduleId])
|
@@index([scheduleId])
|
||||||
}
|
}
|
||||||
@@ -374,6 +408,9 @@ model SelectedCalendar {
|
|||||||
externalId String
|
externalId String
|
||||||
|
|
||||||
@@id([userId, integration, externalId])
|
@@id([userId, integration, externalId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([integration])
|
||||||
|
@@index([externalId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum EventTypeCustomInputType {
|
enum EventTypeCustomInputType {
|
||||||
@@ -395,6 +432,8 @@ model EventTypeCustomInput {
|
|||||||
options Json?
|
options Json?
|
||||||
required Boolean
|
required Boolean
|
||||||
placeholder String @default("")
|
placeholder String @default("")
|
||||||
|
|
||||||
|
@@index([eventTypeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model ResetPasswordRequest {
|
model ResetPasswordRequest {
|
||||||
@@ -415,6 +454,9 @@ model ReminderMail {
|
|||||||
reminderType ReminderType
|
reminderType ReminderType
|
||||||
elapsedMinutes Int
|
elapsedMinutes Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([referenceId])
|
||||||
|
@@index([reminderType])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Payment {
|
model Payment {
|
||||||
@@ -432,6 +474,9 @@ model Payment {
|
|||||||
data Json
|
data Json
|
||||||
externalId String @unique
|
externalId String @unique
|
||||||
paymentOption PaymentOption? @default(ON_BOOKING)
|
paymentOption PaymentOption? @default(ON_BOOKING)
|
||||||
|
|
||||||
|
@@index([bookingId])
|
||||||
|
@@index([externalId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum PaymentOption {
|
enum PaymentOption {
|
||||||
@@ -486,6 +531,8 @@ model ApiKey {
|
|||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
app App? @relation(fields: [appId], references: [slug], onDelete: Cascade)
|
||||||
appId String?
|
appId String?
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model HashedLink {
|
model HashedLink {
|
||||||
@@ -512,6 +559,8 @@ model Account {
|
|||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([provider, providerAccountId])
|
@@unique([provider, providerAccountId])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([type])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
@@ -520,6 +569,8 @@ model Session {
|
|||||||
userId Int
|
userId Int
|
||||||
expires DateTime
|
expires DateTime
|
||||||
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AppCategories {
|
enum AppCategories {
|
||||||
@@ -549,6 +600,8 @@ model App {
|
|||||||
Webhook Webhook[]
|
Webhook Webhook[]
|
||||||
ApiKey ApiKey[]
|
ApiKey ApiKey[]
|
||||||
enabled Boolean @default(false)
|
enabled Boolean @default(false)
|
||||||
|
|
||||||
|
@@index([enabled])
|
||||||
}
|
}
|
||||||
|
|
||||||
model App_RoutingForms_Form {
|
model App_RoutingForms_Form {
|
||||||
@@ -565,6 +618,9 @@ model App_RoutingForms_Form {
|
|||||||
disabled Boolean @default(false)
|
disabled Boolean @default(false)
|
||||||
/// @zod.custom(imports.RoutingFormSettings)
|
/// @zod.custom(imports.RoutingFormSettings)
|
||||||
settings Json?
|
settings Json?
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([disabled])
|
||||||
}
|
}
|
||||||
|
|
||||||
model App_RoutingForms_FormResponse {
|
model App_RoutingForms_FormResponse {
|
||||||
@@ -576,6 +632,8 @@ model App_RoutingForms_FormResponse {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@unique([formFillerId, formId])
|
@@unique([formFillerId, formId])
|
||||||
|
@@index([formFillerId])
|
||||||
|
@@index([formId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Feedback {
|
model Feedback {
|
||||||
@@ -585,6 +643,9 @@ model Feedback {
|
|||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
rating String
|
rating String
|
||||||
comment String?
|
comment String?
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([rating])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WorkflowTriggerEvents {
|
enum WorkflowTriggerEvents {
|
||||||
@@ -617,6 +678,8 @@ model WorkflowStep {
|
|||||||
numberRequired Boolean?
|
numberRequired Boolean?
|
||||||
sender String?
|
sender String?
|
||||||
numberVerificationPending Boolean @default(true)
|
numberVerificationPending Boolean @default(true)
|
||||||
|
|
||||||
|
@@index([workflowId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Workflow {
|
model Workflow {
|
||||||
@@ -631,6 +694,9 @@ model Workflow {
|
|||||||
time Int?
|
time Int?
|
||||||
timeUnit TimeUnit?
|
timeUnit TimeUnit?
|
||||||
steps WorkflowStep[]
|
steps WorkflowStep[]
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model WorkflowsOnEventTypes {
|
model WorkflowsOnEventTypes {
|
||||||
@@ -639,6 +705,9 @@ model WorkflowsOnEventTypes {
|
|||||||
workflowId Int
|
workflowId Int
|
||||||
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
|
||||||
eventTypeId Int
|
eventTypeId Int
|
||||||
|
|
||||||
|
@@index([workflowId])
|
||||||
|
@@index([eventTypeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Deployment {
|
model Deployment {
|
||||||
@@ -668,6 +737,9 @@ model WorkflowReminder {
|
|||||||
workflowStepId Int?
|
workflowStepId Int?
|
||||||
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
|
workflowStep WorkflowStep? @relation(fields: [workflowStepId], references: [id])
|
||||||
cancelled Boolean?
|
cancelled Boolean?
|
||||||
|
|
||||||
|
@@index([bookingUid])
|
||||||
|
@@index([workflowStepId])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum WorkflowTemplates {
|
enum WorkflowTemplates {
|
||||||
@@ -688,6 +760,9 @@ model BookingSeat {
|
|||||||
attendeeId Int @unique
|
attendeeId Int @unique
|
||||||
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
|
attendee Attendee @relation(fields: [attendeeId], references: [id], onDelete: Cascade)
|
||||||
data Json?
|
data Json?
|
||||||
|
|
||||||
|
@@index([bookingId])
|
||||||
|
@@index([attendeeId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model VerifiedNumber {
|
model VerifiedNumber {
|
||||||
@@ -697,6 +772,9 @@ model VerifiedNumber {
|
|||||||
teamId Int?
|
teamId Int?
|
||||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||||
phoneNumber String
|
phoneNumber String
|
||||||
|
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Feature {
|
model Feature {
|
||||||
@@ -714,6 +792,9 @@ model Feature {
|
|||||||
createdAt DateTime? @default(now())
|
createdAt DateTime? @default(now())
|
||||||
updatedAt DateTime? @default(now()) @updatedAt
|
updatedAt DateTime? @default(now()) @updatedAt
|
||||||
updatedBy Int?
|
updatedBy Int?
|
||||||
|
|
||||||
|
@@index([enabled])
|
||||||
|
@@index([stale])
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FeatureType {
|
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({
|
const eventType = await prisma.eventType.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: booking.eventTypeId || undefined,
|
id: booking.eventTypeId,
|
||||||
schedulingType: SchedulingType.COLLECTIVE,
|
schedulingType: SchedulingType.COLLECTIVE,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
@@ -791,15 +792,16 @@ export const bookingsRouter = router({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return eventType && eventType.users.find((user) => booking.userId === user.id);
|
if (eventType && !eventType.users.find((user) => booking.userId === user.id)) {
|
||||||
};
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
if (booking.userId !== user.id && !(await authorized())) {
|
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "UNAUTHORIZED" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
// Do not move this before authorization check.
|
||||||
if (isConfirmed) throw new TRPCError({ code: "BAD_REQUEST", message: "Booking already confirmed" });
|
// 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 booking requires payment and is not paid, we don't allow confirmation
|
||||||
if (confirmed && booking.payment.length > 0 && !booking.paid) {
|
if (confirmed && booking.payment.length > 0 && !booking.paid) {
|
||||||
@@ -815,14 +817,22 @@ export const bookingsRouter = router({
|
|||||||
return { message: "Booking confirmed", status: BookingStatus.ACCEPTED };
|
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 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 {
|
return {
|
||||||
name: attendee.name,
|
name: attendee.name,
|
||||||
email: attendee.email,
|
email: attendee.email,
|
||||||
timeZone: attendee.timeZone,
|
timeZone: attendee.timeZone,
|
||||||
language: {
|
language: {
|
||||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
translate,
|
||||||
locale: attendee.locale ?? "en",
|
locale,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -864,6 +874,9 @@ export const bookingsRouter = router({
|
|||||||
recurringEventId,
|
recurringEventId,
|
||||||
id: booking.id,
|
id: booking.id,
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
// FIXME: It might be best to retrieve recurringEventId from the booking itself.
|
// FIXME: It might be best to retrieve recurringEventId from the booking itself.
|
||||||
|
|||||||
@@ -25,9 +25,10 @@ export type VerticalTabItemProps = {
|
|||||||
isExternalLink?: boolean;
|
isExternalLink?: boolean;
|
||||||
linkProps?: Omit<ComponentProps<typeof Link>, "href">;
|
linkProps?: Omit<ComponentProps<typeof Link>, "href">;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
iconClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const VerticalTabItem = function ({
|
const VerticalTabItem = ({
|
||||||
name,
|
name,
|
||||||
href,
|
href,
|
||||||
info,
|
info,
|
||||||
@@ -35,11 +36,10 @@ const VerticalTabItem = function ({
|
|||||||
disableChevron,
|
disableChevron,
|
||||||
linkProps,
|
linkProps,
|
||||||
...props
|
...props
|
||||||
}: VerticalTabItemProps) {
|
}: VerticalTabItemProps) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { asPath } = useRouter();
|
const { asPath } = useRouter();
|
||||||
const isCurrent = asPath.startsWith(href);
|
const isCurrent = asPath.startsWith(href);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={name}>
|
<Fragment key={name}>
|
||||||
{!props.hidden && (
|
{!props.hidden && (
|
||||||
@@ -60,7 +60,12 @@ const VerticalTabItem = function ({
|
|||||||
data-testid={`vertical-tab-${name}`}
|
data-testid={`vertical-tab-${name}`}
|
||||||
aria-current={isCurrent ? "page" : undefined}>
|
aria-current={isCurrent ? "page" : undefined}>
|
||||||
{props.icon && (
|
{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">
|
<div className="h-fit">
|
||||||
<span className="flex items-center space-x-2 rtl:space-x-reverse">
|
<span className="flex items-center space-x-2 rtl:space-x-reverse">
|
||||||
|
|||||||
@@ -12,9 +12,18 @@ export interface NavTabProps {
|
|||||||
sticky?: boolean;
|
sticky?: boolean;
|
||||||
linkProps?: VerticalTabItemProps["linkProps"];
|
linkProps?: VerticalTabItemProps["linkProps"];
|
||||||
itemClassname?: string;
|
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 (
|
return (
|
||||||
<nav
|
<nav
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -27,7 +36,13 @@ const NavTabs = function ({ tabs, className = "", sticky, linkProps, itemClassna
|
|||||||
{sticky && <div className="pt-6" />}
|
{sticky && <div className="pt-6" />}
|
||||||
{props.children}
|
{props.children}
|
||||||
{tabs.map((tab, idx) => (
|
{tabs.map((tab, idx) => (
|
||||||
<VerticalTabItem {...tab} key={idx} linkProps={linkProps} className={itemClassname} />
|
<VerticalTabItem
|
||||||
|
{...tab}
|
||||||
|
key={idx}
|
||||||
|
linkProps={linkProps}
|
||||||
|
className={itemClassname}
|
||||||
|
iconClassName={iconClassName}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user