feat: advanced tab platform wrapper (#16941)

* fixed error in advanced tab atom build

-- used `DOMPurify` instead of `sanitize-html`

* hide BookerLayoutSelector for platform and removePlatformClientIdFromEmail

* fixup! Merge branch 'main' into event-advanced-platform-wrapper

* undo linting changes

* undo linting changes

* fix: active state of tabs items

---------

Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Morgan Vernay <morgan@cal.com>
This commit is contained in:
Somay Chauhan
2024-10-10 17:09:03 +03:00
committed by GitHub
co-authored by Morgan Morgan Vernay
parent 1f08bc5a4f
commit 99caae8665
17 changed files with 208 additions and 101 deletions
+1 -1
View File
@@ -28,7 +28,7 @@
"dependencies": {
"@calcom/platform-constants": "*",
"@calcom/platform-enums": "*",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.44",
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.45",
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
"@calcom/platform-types": "*",
"@calcom/platform-utils": "*",
+2
View File
@@ -79,6 +79,7 @@
"bcp-47-match": "^2.0.3",
"bcryptjs": "^2.4.3",
"classnames": "^2.3.1",
"dompurify": "^3.1.7",
"dotenv-cli": "^6.0.0",
"entities": "^4.4.0",
"eslint-config-next": "^13.2.1",
@@ -152,6 +153,7 @@
"@types/async": "^3.2.15",
"@types/bcryptjs": "^2.4.2",
"@types/detect-port": "^1.3.2",
"@types/dompurify": "^3.0.5",
"@types/glidejs__glide": "^3.4.2",
"@types/lodash": "^4.14.182",
"@types/markdown-it": "^12.2.3",
@@ -34,14 +34,20 @@ async function enableContactOwnerOverride(page: Page) {
async function selectFirstAttributeOption({ fromLocator }: { fromLocator: Locator }) {
await selectOptionUsingLocator({
locator: fromLocator,
option: 1
option: 1,
});
}
async function selectFirstValueForAttributeValue({ fromLocator, option }: { fromLocator: Locator, option: number }) {
async function selectFirstValueForAttributeValue({
fromLocator,
option,
}: {
fromLocator: Locator;
option: number;
}) {
await selectOptionUsingLocator({
locator: fromLocator,
option
option,
});
}
@@ -59,7 +65,7 @@ async function addAttributeRoutingRule(page: Page) {
await selectFirstValueForAttributeValue({
fromLocator: attributeValueSelector,
// Select 'Value of Field Short Text' option
option: numOfOptionsInAttribute + 1
option: numOfOptionsInAttribute + 1,
});
}
@@ -571,7 +577,7 @@ test.describe("Routing Forms", () => {
await page.click('[data-testid="test-preview"]');
await page.fill('[data-testid="form-field-short-text"]', "large");
await page.click('[data-testid="test-routing"]');
await page.waitForSelector('text=@example.com');
await page.waitForSelector("text=@example.com");
await page.click('[data-testid="dialog-rejection"]');
})();
@@ -579,9 +585,8 @@ test.describe("Routing Forms", () => {
await page.click('[data-testid="test-preview"]');
await page.fill('[data-testid="form-field-short-text"]', "medium");
await page.click('[data-testid="test-routing"]');
await page.waitForSelector('text=No matching members.');
await page.waitForSelector("text=No matching members.");
await page.click('[data-testid="dialog-rejection"]');
})();
});
});
@@ -751,15 +756,12 @@ async function selectOption({
.click();
}
export async function selectOptionUsingLocator({
locator,
option,
}: {
locator: Locator;
option: number;
}) {
export async function selectOptionUsingLocator({ locator, option }: { locator: Locator; option: number }) {
await locator.click();
await locator.locator('[id*="react-select-"][aria-disabled]').nth(option - 1).click();
await locator
.locator('[id*="react-select-"][aria-disabled]')
.nth(option - 1)
.click();
}
async function verifyFieldOptionsInRule(options: string[], page: Page) {
@@ -2,7 +2,6 @@ import { v4 as uuidv4 } from "uuid";
import dayjs from "@calcom/dayjs";
import { getRoutedTeamMemberIdsFromSearchParams } from "@calcom/lib/bookings/getRoutedTeamMemberIdsFromSearchParams";
import { fromEntriesWithDuplicateKeys } from "@calcom/lib/fromEntriesWithDuplicateKeys";
import { parseRecurringDates } from "@calcom/lib/parse-dates";
import type { BookerEvent, BookingCreateBody, RecurringBookingCreateBody } from "../../types";
@@ -39,7 +39,13 @@ export async function loadAndValidateUsers({
routedTeamMemberIds,
contactOwnerEmail,
}: InputProps): Promise<Users> {
let users: Users = await loadUsers({ eventType, dynamicUserList, req, routedTeamMemberIds, contactOwnerEmail });
let users: Users = await loadUsers({
eventType,
dynamicUserList,
req,
routedTeamMemberIds,
contactOwnerEmail,
});
const isDynamicAllowed = !users.some((user) => !user.allowDynamicBooking);
if (!isDynamicAllowed && !eventTypeId) {
logger.warn({
@@ -1,9 +1,8 @@
import dynamic from "next/dynamic";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { z } from "zod";
import type { EventAdvancedWebWrapperProps } from "@calcom/atoms/event-types/wrappers/EventAdvancedWebWrapper";
import { useAtomsContext, useIsPlatform } from "@calcom/atoms/monorepo";
import type { EventNameObjectType } from "@calcom/core/event";
import { getEventName } from "@calcom/core/event";
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
@@ -14,7 +13,7 @@ import {
allowDisablingHostConfirmationEmails,
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
import { MultiplePrivateLinksController } from "@calcom/features/eventtypes/components";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import type { FormValues, EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
import type { fieldSchema } from "@calcom/features/form-builder/schema";
import type { EditableSchema } from "@calcom/features/form-builder/schema";
@@ -39,21 +38,26 @@ import {
SettingsToggle,
Switch,
TextField,
showToast,
ColorPicker,
} from "@calcom/ui";
import CustomEventTypeModal from "./CustomEventTypeModal";
import { DisableAllEmailsSetting } from "./DisableAllEmailsSetting";
import RequiresConfirmationController from "./RequiresConfirmationController";
type BookingField = z.infer<typeof fieldSchema>;
const CustomEventTypeModal = dynamic(() => import("./CustomEventTypeModal"));
export type EventAdvancedTabProps = EventAdvancedWebWrapperProps & {
calendarsQueryData?: RouterOutputs["viewer"]["connectedCalendars"];
user?: RouterOutputs["viewer"]["me"];
export type EventAdvancedBaseProps = Pick<EventTypeSetupProps, "eventType" | "team"> & {
user?: Partial<
Pick<RouterOutputs["viewer"]["me"], "email" | "secondaryEmails" | "theme" | "defaultBookerLayouts">
>;
isUserLoading?: boolean;
showToast: (message: string, variant: "success" | "warning" | "error") => void;
};
export type EventAdvancedTabProps = EventAdvancedBaseProps & {
calendarsQueryData?: RouterOutputs["viewer"]["connectedCalendars"];
showBookerLayoutSelector: boolean;
};
export const EventAdvancedTab = ({
@@ -62,7 +66,11 @@ export const EventAdvancedTab = ({
calendarsQueryData,
user,
isUserLoading,
showToast,
showBookerLayoutSelector,
}: EventAdvancedTabProps) => {
const isPlatform = useIsPlatform();
const platformContext = useAtomsContext();
const formMethods = useFormContext<FormValues>();
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
@@ -169,7 +177,7 @@ export const EventAdvancedTab = ({
const displayDestinationCalendarSelector =
!!calendarsQueryData?.connectedCalendars?.length && (!team || isChildrenManagedEventType);
const verifiedSecondaryEmails = [
let verifiedSecondaryEmails = [
{
label: user?.email || "",
value: -1,
@@ -178,6 +186,20 @@ export const EventAdvancedTab = ({
.filter((secondaryEmail) => !!secondaryEmail.emailVerified)
.map((secondaryEmail) => ({ label: secondaryEmail.email, value: secondaryEmail.id })),
];
const removePlatformClientIdFromEmail = (email: string, clientId: string) =>
email.replace(`+${clientId}`, "");
let userEmail = user?.email || "";
if (isPlatform && platformContext.clientId) {
verifiedSecondaryEmails = verifiedSecondaryEmails.map((email) => ({
...email,
label: removePlatformClientIdFromEmail(email.label, platformContext.clientId),
}));
userEmail = removePlatformClientIdFromEmail(userEmail, platformContext.clientId);
}
const selectedSecondaryEmailId = formMethods.getValues("secondaryEmailId") || -1;
return (
<div className="flex flex-col space-y-4">
@@ -257,7 +279,7 @@ export const EventAdvancedTab = ({
placeholder={
selectedSecondaryEmailId === -1 && (
<span className="text-default min-w-0 overflow-hidden truncate whitespace-nowrap">
<Badge variant="blue">{t("default")}</Badge> {user?.email || ""}
<Badge variant="blue">{t("default")}</Badge> {userEmail}
</span>
)
}
@@ -275,13 +297,15 @@ export const EventAdvancedTab = ({
)}
</div>
</div>
<BookerLayoutSelector
fallbackToUserSettings
isDark={selectedThemeIsDark}
isOuterBorder={true}
user={user}
isUserLoading={isUserLoading}
/>
{showBookerLayoutSelector && (
<BookerLayoutSelector
fallbackToUserSettings
isDark={selectedThemeIsDark}
isOuterBorder={true}
user={user}
isUserLoading={isUserLoading}
/>
)}
<div className="border-subtle space-y-6 rounded-lg border p-6">
<FormBuilder
title={t("booking_questions_title")}
@@ -419,45 +443,47 @@ export const EventAdvancedTab = ({
</>
)}
/>
<Controller
name="multiplePrivateLinks"
render={() => {
return (
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
multiplePrivateLinksVisible && "rounded-b-none"
)}
childrenClassName="lg:ml-0"
data-testid="multiplePrivateLinksCheck"
title={t("multiple_private_links_title")}
{...multiplePrivateLinksLocked}
description={t("multiple_private_links_description", { appName: APP_NAME })}
tooltip={isManagedEventType ? t("managed_event_field_parent_control_disabled") : ""}
checked={multiplePrivateLinksVisible}
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("multiplePrivateLinks", [], { shouldDirty: true });
} else {
formMethods.setValue(
"multiplePrivateLinks",
[generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id)],
{ shouldDirty: true }
);
}
setMultiplePrivateLinksVisible(e);
}}>
{!isManagedEventType && (
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<MultiplePrivateLinksController team={team} bookerUrl={eventType.bookerUrl} />
</div>
)}
</SettingsToggle>
);
}}
/>
{!isPlatform && (
<Controller
name="multiplePrivateLinks"
render={() => {
return (
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
multiplePrivateLinksVisible && "rounded-b-none"
)}
childrenClassName="lg:ml-0"
data-testid="multiplePrivateLinksCheck"
title={t("multiple_private_links_title")}
{...multiplePrivateLinksLocked}
description={t("multiple_private_links_description", { appName: APP_NAME })}
tooltip={isManagedEventType ? t("managed_event_field_parent_control_disabled") : ""}
checked={multiplePrivateLinksVisible}
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("multiplePrivateLinks", [], { shouldDirty: true });
} else {
formMethods.setValue(
"multiplePrivateLinks",
[generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id)],
{ shouldDirty: true }
);
}
setMultiplePrivateLinksVisible(e);
}}>
{!isManagedEventType && (
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<MultiplePrivateLinksController team={team} bookerUrl={eventType.bookerUrl} />
</div>
)}
</SettingsToggle>
);
}}
/>
)}
<Controller
name="seatsPerTimeSlotEnabled"
render={({ field: { value, onChange } }) => (
@@ -7,7 +7,7 @@ import type { z } from "zod";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { md } from "@calcom/lib/markdownIt";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { markdownToSafeHTMLClient } from "@calcom/lib/markdownToSafeHTMLClient";
import turndown from "@calcom/lib/turndownService";
import {
Badge,
@@ -153,7 +153,7 @@ export const FormBuilder = function FormBuilder({
}
if (fieldsThatSupportLabelAsSafeHtml.includes(field.type)) {
field = { ...field, labelAsSafeHtml: markdownToSafeHTML(field.label ?? "") };
field = { ...field, labelAsSafeHtml: markdownToSafeHTMLClient(field.label ?? "") };
}
const numOptions = options?.length ?? 0;
const firstOptionInput =
@@ -692,7 +692,7 @@ function FieldLabel({ field }: { field: RhfFormField }) {
<span
dangerouslySetInnerHTML={{
// Derive from field.label because label might change in b/w and field.labelAsSafeHtml will not be updated.
__html: markdownToSafeHTML(field.label || "") || t(field.defaultLabel || ""),
__html: markdownToSafeHTMLClient(field.label || "") || t(field.defaultLabel || ""),
}}
/>
);
@@ -34,7 +34,7 @@ type BookerLayoutSelectorProps = {
isLoading?: boolean;
isDisabled?: boolean;
isOuterBorder?: boolean;
user?: RouterOutputs["viewer"]["me"];
user?: Partial<Pick<RouterOutputs["viewer"]["me"], "defaultBookerLayouts">>;
isUserLoading?: boolean;
};
@@ -104,7 +104,7 @@ type BookerLayoutFieldsProps = {
showUserSettings: boolean;
isDark?: boolean;
isOuterBorder?: boolean;
user?: RouterOutputs["viewer"]["me"];
user?: Partial<Pick<RouterOutputs["viewer"]["me"], "defaultBookerLayouts">>;
isUserLoading?: boolean;
};
+1 -1
View File
@@ -21,4 +21,4 @@ export function fromEntriesWithDuplicateKeys(entries: IterableIterator<[string,
}
}
return result;
}
}
+30
View File
@@ -0,0 +1,30 @@
import DOMPurify from "dompurify";
import { md } from "@calcom/lib/markdownIt";
if (typeof window == "undefined") {
console.warn(
"`markdownToSafeHTMLClient` should not be used on the server side. use markdownToSafeHTML instead"
);
}
export function markdownToSafeHTMLClient(markdown: string | null) {
if (!markdown) return "";
const html = md.render(markdown);
const safeHTML = DOMPurify.sanitize(html);
const safeHTMLWithListFormatting = safeHTML
.replace(
/<ul>/g,
"<ul style='list-style-type: disc; list-style-position: inside; margin-left: 12px; margin-bottom: 4px'>"
)
.replace(
/<ol>/g,
"<ol style='list-style-type: decimal; list-style-position: inside; margin-left: 12px; margin-bottom: 4px'>"
)
.replace(/<a\s+href=/g, "<a target='_blank' class='text-blue-500 hover:text-blue-600' href=");
return safeHTMLWithListFormatting;
}
@@ -60,12 +60,14 @@ export const usePlatformTabsNavigations = ({ formMethods, eventType, team, tabs
onClick: (tab) => {
setCurrentTab(tab);
},
currentTab,
});
if (!requirePayment) {
navigation.splice(3, 0, {
name: "recurring",
onClick: () => setCurrentTab("recurring"),
isActive: currentTab === "recurring",
href: `${url}?tabName=recurring`,
icon: "repeat",
info: `recurring_event_tab_description`,
@@ -75,6 +77,7 @@ export const usePlatformTabsNavigations = ({ formMethods, eventType, team, tabs
navigation.splice(1, 0, {
name: "availability",
onClick: () => setCurrentTab("availability"),
isActive: currentTab === "availability",
href: `${url}?tabName=availability`,
icon: "calendar",
info:
@@ -96,6 +99,7 @@ export const usePlatformTabsNavigations = ({ formMethods, eventType, team, tabs
navigation.splice(2, 0, {
name: "assignment",
onClick: () => setCurrentTab("team"),
isActive: currentTab === "team",
href: `${url}?tabName=team`,
icon: "users",
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
@@ -133,15 +137,17 @@ type getNavigationProps = {
tabs: PlatformTabs[];
url: string;
onClick: (tab: PlatformTabs) => void;
currentTab: PlatformTabs;
};
function getNavigation({ length, multipleDuration, t, tabs, url, onClick }: getNavigationProps) {
function getNavigation({ length, multipleDuration, t, tabs, url, onClick, currentTab }: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
const tabsNavigation: VerticalTabItemProps[] = [];
tabs.includes("setup") &&
tabsNavigation.push({
name: "event_setup_tab_title",
onClick: () => onClick("setup"),
isActive: currentTab === "setup",
href: `${url}?tabName=setup`,
icon: "link",
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
@@ -150,6 +156,7 @@ function getNavigation({ length, multipleDuration, t, tabs, url, onClick }: getN
tabsNavigation.push({
name: "event_limit_tab_title",
onClick: () => onClick("limits"),
isActive: currentTab === "limits",
href: `${url}?tabName=limits`,
icon: "clock",
info: `event_limit_tab_description`,
@@ -159,6 +166,7 @@ function getNavigation({ length, multipleDuration, t, tabs, url, onClick }: getN
tabsNavigation.push({
name: "event_advanced_tab_title",
onClick: () => onClick("advanced"),
isActive: currentTab === "advanced",
href: `${url}?tabName=advanced`,
icon: "sliders-vertical",
info: `event_advanced_tab_description`,
@@ -0,0 +1,17 @@
import type { EventAdvancedBaseProps } from "@calcom/features/eventtypes/components/tabs/advanced/EventAdvancedTab";
import { EventAdvancedTab } from "@calcom/features/eventtypes/components/tabs/advanced/EventAdvancedTab";
import { useConnectedCalendars } from "../../hooks/useConnectedCalendars";
const EventAdvancedPlatformWrapper = (props: EventAdvancedBaseProps) => {
const { data: connectedCalendarsQuery } = useConnectedCalendars({});
return (
<EventAdvancedTab
{...props}
calendarsQueryData={connectedCalendarsQuery}
showBookerLayoutSelector={false}
/>
);
};
export default EventAdvancedPlatformWrapper;
@@ -1,25 +1,14 @@
import type { EventAdvancedBaseProps } from "@calcom/features/eventtypes/components/tabs/advanced/EventAdvancedTab";
import { EventAdvancedTab } from "@calcom/features/eventtypes/components/tabs/advanced/EventAdvancedTab";
import type { EventTypeSetupProps } from "@calcom/features/eventtypes/lib/types";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
export type EventAdvancedWebWrapperProps = Pick<EventTypeSetupProps, "eventType" | "team"> & {
loggedInUser?: RouterOutputs["viewer"]["me"];
isLoggedInUserPending?: boolean;
};
const EventAdvancedWebWrapper = ({
loggedInUser,
isLoggedInUserPending,
...props
}: EventAdvancedWebWrapperProps) => {
const EventAdvancedWebWrapper = ({ ...props }: EventAdvancedBaseProps) => {
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
return (
<EventAdvancedTab
{...props}
calendarsQueryData={connectedCalendarsQuery.data}
user={loggedInUser}
isUserLoading={isLoggedInUserPending}
showBookerLayoutSelector={true}
/>
);
};
@@ -10,6 +10,7 @@ import type { EventTypeSetupProps, FormValues, TabMap } from "@calcom/features/e
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { useMe } from "../../hooks/useMe";
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
import { useToast } from "../../src/components/ui/use-toast";
import { useAtomsEventTypeById, QUERY_KEY as ATOM_EVENT_TYPE_QUERY_KEY } from "../hooks/useAtomEventTypeById";
@@ -17,6 +18,7 @@ import { useAtomUpdateEventType } from "../hooks/useAtomUpdateEventType";
import { useEventTypeForm } from "../hooks/useEventTypeForm";
import { useHandleRouteChange } from "../hooks/useHandleRouteChange";
import { usePlatformTabsNavigations } from "../hooks/usePlatformTabsNavigations";
import EventAdvancedPlatformWrapper from "./EventAdvancedPlatformWrapper";
import EventLimitsTabPlatformWrapper from "./EventLimitsTabPlatformWrapper";
import EventRecurringTabPlatformWrapper from "./EventRecurringTabPlatformWrapper";
import SetupTab from "./EventSetupTabPlatformWrapper";
@@ -42,6 +44,7 @@ const EventType = ({
const [pendingRoute, setPendingRoute] = useState("");
const { eventType, locationOptions, team, teamMembers, destinationCalendar } = props;
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
const { data: user, isLoading: isUserLoading } = useMe();
const updateMutation = useAtomUpdateEventType({
onSuccess: async () => {
@@ -73,6 +76,10 @@ const EventType = ({
const { form, handleSubmit } = useEventTypeForm({ eventType, onSubmit: updateMutation.mutate });
const slug = form.watch("slug") ?? eventType.slug;
const showToast = (message: string, variant: "success" | "warning" | "error") => {
toast({ description: message });
};
const tabMap = {
setup: tabs.includes("setup") ? (
<SetupTab
@@ -87,8 +94,19 @@ const EventType = ({
),
availability: <></>,
team: <></>,
advanced: tabs.includes("advanced") ? (
<EventAdvancedPlatformWrapper
eventType={eventType}
team={team}
user={user?.data}
isUserLoading={isUserLoading}
showToast={showToast}
/>
) : (
<></>
),
limits: tabs.includes("limits") ? <EventLimitsTabPlatformWrapper eventType={eventType} /> : <></>,
advanced: <></>,
instant: <></>,
recurring: tabs.includes("recurring") ? (
<EventRecurringTabPlatformWrapper eventType={eventType} />
@@ -250,8 +250,9 @@ const EventTypeWeb = ({
<EventAdvancedTab
eventType={eventType}
team={team}
loggedInUser={loggedInUser}
isLoggedInUserPending={isLoggedInUserPending}
user={loggedInUser}
isUserLoading={isLoggedInUserPending}
showToast={showToast}
/>
),
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
@@ -18,6 +18,8 @@ export type HorizontalTabItemProps = {
linkScroll?: boolean;
icon?: IconName;
avatar?: string;
onClick?: (name: string) => void;
isActive?: boolean;
};
const HorizontalTabItem = function ({
@@ -30,10 +32,16 @@ const HorizontalTabItem = function ({
}: HorizontalTabItemProps) {
const { t, isLocaleReady } = useLocale();
const isCurrent = useUrlMatchesCurrentUrl(href);
const isCurrent = useUrlMatchesCurrentUrl(href) || props?.isActive;
return (
<Link
onClick={(e) => {
if (props.onClick) {
e.preventDefault();
props.onClick(name);
}
}}
key={name}
href={href}
shallow={linkShallow}
@@ -26,6 +26,7 @@ export type VerticalTabItemProps = {
avatar?: string;
iconClassName?: string;
onClick?: (name: string) => void;
isActive?: boolean;
};
const VerticalTabItem = ({
@@ -39,7 +40,7 @@ const VerticalTabItem = ({
...props
}: VerticalTabItemProps) => {
const { t } = useLocale();
const isCurrent = useUrlMatchesCurrentUrl(href);
const isCurrent = useUrlMatchesCurrentUrl(href) || props?.isActive;
return (
<Fragment key={name}>