refactor: migrate from Headless UI Tabs to custom Tabs component

- Replaced instances of Headless UI's Tab component with a new custom Tabs component across various components, including analytics modals, image pickers, and navigation panes.
- Updated tab handling logic to align with the new Tabs API, ensuring consistent behavior and styling.
- Removed unused Tab imports and cleaned up related code for improved maintainability.
- This refactor enhances the overall structure and consistency of tab navigation within the application.
This commit is contained in:
Jayash Tripathy
2025-12-03 17:37:45 +05:30
parent 36d42856e9
commit 2acba2980b
14 changed files with 179 additions and 368 deletions

View File

@@ -69,13 +69,11 @@ export const WorkItemsModalMainContent = observer(function WorkItemsModalMainCon
); );
return ( return (
<Tab.Group as={React.Fragment}> <div className="flex flex-col gap-14 overflow-y-auto p-6">
<div className="flex flex-col gap-14 overflow-y-auto p-6"> <TotalInsights analyticsType="work-items" peekView={!fullScreen} />
<TotalInsights analyticsType="work-items" peekView={!fullScreen} /> <CreatedVsResolved />
<CreatedVsResolved /> <CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} /> <WorkItemsInsightTable />
<WorkItemsInsightTable /> </div>
</div>
</Tab.Group>
); );
}); });

View File

@@ -5,11 +5,12 @@ import { useDropzone } from "react-dropzone";
import type { Control } from "react-hook-form"; import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
import useSWR from "swr"; import useSWR from "swr";
import { Tab, Popover } from "@headlessui/react"; import { Popover } from "@headlessui/react";
// plane imports // plane imports
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks"; import { useOutsideClickDetector } from "@plane/hooks";
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { Tabs } from "@plane/propel/tabs";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types"; import { EFileAssetType } from "@plane/types";
import { Input, Loader } from "@plane/ui"; import { Input, Loader } from "@plane/ui";
@@ -197,91 +198,88 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
ref={imagePickerRef} ref={imagePickerRef}
className="flex h-96 w-80 flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl md:h-[28rem] md:w-[36rem]" className="flex h-96 w-80 flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl md:h-[28rem] md:w-[36rem]"
> >
<Tab.Group> <Tabs defaultValue={tabOptions[0].key} className={"h-full overflow-hidden"}>
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1"> <Tabs.List className="p-1 w-fit">
{tabOptions.map((tab) => ( {tabOptions.map((tab) => (
<Tab <Tabs.Trigger
key={tab.key} key={tab.key}
className={({ selected }) => value={tab.key}
`rounded px-4 py-1 text-center text-sm outline-none transition-colors ${ className="rounded px-4 py-1 text-center text-sm outline-none transition-colors data-[selected]:bg-custom-primary data-[selected]:text-white text-custom-text-100"
selected ? "bg-custom-primary text-white" : "text-custom-text-100"
}`
}
> >
{tab.title} {tab.title}
</Tab> </Tabs.Trigger>
))} ))}
</Tab.List> </Tabs.List>
<Tab.Panels className="vertical-scrollbar scrollbar-md h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="mt-4 h-full w-full space-y-4"> <div className="mt-4 flex-1 overflow-auto">
{(unsplashImages || !unsplashError) && ( {(unsplashImages || !unsplashError) && (
<> <Tabs.Content className="h-full w-full space-y-4" value="unsplash">
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<Controller <Controller
control={control} control={control}
name="search" name="search"
render={({ field: { value, ref } }) => ( render={({ field: { value, ref } }) => (
<Input <Input
id="search" id="search"
name="search" name="search"
type="text" type="text"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
setSearchParams(formData.search); setSearchParams(formData.search);
} }
}}
value={value}
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
ref={ref}
placeholder="Search for images"
className="w-full text-sm"
/>
)}
/>
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm">
Search
</Button>
</div>
{unsplashImages ? (
unsplashImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{unsplashImages.map((image) => (
<div
key={image.id}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
}} }}
value={value} >
onChange={(e) => setFormData({ ...formData, search: e.target.value })} <img
ref={ref} src={image.urls.small}
placeholder="Search for images" alt={image.alt_description}
className="w-full text-sm" className="absolute left-0 top-0 h-full w-full cursor-pointer rounded object-cover"
/> />
)} </div>
/> ))}
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm"> </div>
Search
</Button>
</div>
{unsplashImages ? (
unsplashImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{unsplashImages.map((image) => (
<div
key={image.id}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
}}
>
<img
src={image.urls.small}
alt={image.alt_description}
className="absolute left-0 top-0 h-full w-full cursor-pointer rounded object-cover"
/>
</div>
))}
</div>
) : (
<p className="pt-7 text-center text-xs text-custom-text-300">No images found.</p>
)
) : ( ) : (
<Loader className="grid grid-cols-4 gap-4"> <p className="pt-7 text-center text-xs text-custom-text-300">No images found.</p>
<Loader.Item height="80px" width="100%" /> )
<Loader.Item height="80px" width="100%" /> ) : (
<Loader.Item height="80px" width="100%" /> <Loader className="grid grid-cols-4 gap-4">
<Loader.Item height="80px" width="100%" /> <Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" /> <Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" /> <Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" /> <Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" /> <Loader.Item height="80px" width="100%" />
</Loader> <Loader.Item height="80px" width="100%" />
)} <Loader.Item height="80px" width="100%" />
</> <Loader.Item height="80px" width="100%" />
)} </Loader>
</Tab.Panel> )}
<Tab.Panel className="mt-4 h-full w-full space-y-4"> </Tabs.Content>
)}
<Tabs.Content className="h-full w-full space-y-4" value="images">
<div className="grid grid-cols-4 gap-4"> <div className="grid grid-cols-4 gap-4">
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => ( {Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
<div <div
@@ -297,8 +295,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
</div> </div>
))} ))}
</div> </div>
</Tab.Panel> </Tabs.Content>
<Tab.Panel className="mt-4 h-full w-full">
<Tabs.Content className="h-full w-full" value="upload">
<div className="flex h-full w-full flex-col gap-y-2"> <div className="flex h-full w-full flex-col gap-y-2">
<div className="flex w-full flex-1 items-center gap-3"> <div className="flex w-full flex-1 items-center gap-3">
<div <div
@@ -365,9 +364,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
</Button> </Button>
</div> </div>
</div> </div>
</Tab.Panel> </Tabs.Content>
</Tab.Panels> </div>
</Tab.Group> </Tabs>
</div> </div>
</Popover.Panel> </Popover.Panel>
)} )}

View File

@@ -1,8 +1,8 @@
import type { FC } from "react"; import type { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import type { TWorkItemFilterCondition } from "@plane/shared-state"; import type { TWorkItemFilterCondition } from "@plane/shared-state";
import type { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types"; import type { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils"; import { cn, toFilterArray } from "@plane/utils";
@@ -54,8 +54,6 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
`cycle-analytics-tab-${cycleId}`, `cycle-analytics-tab-${cycleId}`,
"stat-assignees" "stat-assignees"
); );
// derived values
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TCycleDistribution; const currentDistribution = distribution as TCycleDistribution;
const currentEstimateDistribution = distribution as TCycleEstimateDistribution; const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[]; const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
@@ -116,9 +114,8 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
return ( return (
<div> <div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}> <Tabs defaultValue={currentTab ?? "stat-assignees"} onValueChange={(value) => setCycleTab(value)}>
<Tab.List <Tabs.List
as="div"
className={cn( className={cn(
`flex w-full items-center justify-between gap-2 rounded-md p-1`, `flex w-full items-center justify-between gap-2 rounded-md p-1`,
roundedTab ? `rounded-3xl` : `rounded-md`, roundedTab ? `rounded-3xl` : `rounded-md`,
@@ -127,23 +124,22 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
)} )}
> >
{PROGRESS_STATS.map((stat) => ( {PROGRESS_STATS.map((stat) => (
<Tab <Tabs.Trigger
key={stat.key}
value={stat.key}
className={cn( className={cn(
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`, `p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`, roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`,
stat.key === currentTab "data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-300",
? "bg-custom-background-100 text-custom-text-300" "text-custom-text-400 hover:text-custom-text-300"
: "text-custom-text-400 hover:text-custom-text-300"
)} )}
key={stat.key}
onClick={() => setCycleTab(stat.key)}
> >
{t(stat.i18n_title)} {t(stat.i18n_title)}
</Tab> </Tabs.Trigger>
))} ))}
</Tab.List> </Tabs.List>
<Tab.Panels className="py-3 text-custom-text-200"> <div className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-states"}> <Tabs.Content value="stat-states">
<StateGroupStatComponent <StateGroupStatComponent
distribution={distributionStateData} distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate} handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
@@ -151,25 +147,25 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
selectedStateGroups={selectedStateGroups} selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount} totalIssuesCount={totalIssuesCount}
/> />
</Tab.Panel> </Tabs.Content>
<Tab.Panel key={"stat-assignees"}> <Tabs.Content value="stat-assignees">
<AssigneeStatComponent <AssigneeStatComponent
distribution={distributionAssigneeData} distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate} handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable} isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds} selectedAssigneeIds={selectedAssigneeIds}
/> />
</Tab.Panel> </Tabs.Content>
<Tab.Panel key={"stat-labels"}> <Tabs.Content value="stat-labels">
<LabelStatComponent <LabelStatComponent
distribution={distributionLabelData} distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate} handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable} isEditable={isEditable}
selectedLabelIds={selectedLabelIds} selectedLabelIds={selectedLabelIds}
/> />
</Tab.Panel> </Tabs.Content>
</Tab.Panels> </div>
</Tab.Group> </Tabs>
</div> </div>
); );
}); });

View File

@@ -2,8 +2,8 @@ import type { FC } from "react";
import { useState } from "react"; import { useState } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { CheckCircle } from "lucide-react"; import { CheckCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports // plane imports
import { Tabs } from "@plane/propel/tabs";
// helpers // helpers
import type { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types"; import type { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types";
import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui"; import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui";
@@ -39,32 +39,27 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
return ( return (
<div className={cn("flex flex-col py-6 px-3", upgradeCardVariantStyle)}> <div className={cn("flex flex-col py-6 px-3", upgradeCardVariantStyle)}>
<Tab.Group selectedIndex={selectedPlan === "month" ? 0 : 1}> <Tabs value={selectedPlan} onValueChange={(value) => setSelectedPlan(value as TBillingFrequency)}>
<div className="flex w-full justify-center h-9"> <div className="flex w-full justify-center">
<Tab.List <Tabs.List className={cn("flex rounded-md w-60", getSubscriptionBackgroundColor(planVariant, "50"))}>
className={cn("flex space-x-1 rounded-md p-0.5 w-60", getSubscriptionBackgroundColor(planVariant, "50"))}
>
{prices.map((price: TSubscriptionPrice) => ( {prices.map((price: TSubscriptionPrice) => (
<Tab <Tabs.Trigger
key={price.key} key={price.key}
className={({ selected }) => value={price.recurring}
cn( className={cn(
"w-full rounded py-1 text-sm font-medium leading-5", "w-full rounded text-sm font-medium leading-5 py-2",
selected "data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-100 data-[selected]:shadow",
? "bg-custom-background-100 text-custom-text-100 shadow" "text-custom-text-300 hover:text-custom-text-200"
: "text-custom-text-300 hover:text-custom-text-200" )}
)
}
onClick={() => setSelectedPlan(price.recurring)}
> >
{renderPriceContent(price)} {renderPriceContent(price)}
</Tab> </Tabs.Trigger>
))} ))}
</Tab.List> </Tabs.List>
</div> </div>
<Tab.Panels> <div>
{prices.map((price: TSubscriptionPrice) => ( {prices.map((price: TSubscriptionPrice) => (
<Tab.Panel key={price.key}> <Tabs.Content key={price.key} value={price.recurring}>
<div className="pt-6 text-center"> <div className="pt-6 text-center">
<div className="text-xl font-medium">Plane {planeName}</div> <div className="text-xl font-medium">Plane {planeName}</div>
{renderActionButton(price)} {renderActionButton(price)}
@@ -88,10 +83,10 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
</ul> </ul>
{extraFeatures && <div>{extraFeatures}</div>} {extraFeatures && <div>{extraFeatures}</div>}
</div> </div>
</Tab.Panel> </Tabs.Content>
))} ))}
</Tab.Panels> </div>
</Tab.Group> </Tabs>
</div> </div>
); );
}); });

View File

@@ -73,7 +73,12 @@ export const PlanUpgradeCard = observer(function PlanUpgradeCard(props: PlanUpgr
<> <>
Yearly Yearly
{yearlyDiscount > 0 && ( {yearlyDiscount > 0 && (
<span className={cn(getDiscountPillStyle(planVariant), "rounded-full px-1.5 py-0.5 ml-1 text-xs")}> <span
className={cn(
getDiscountPillStyle(planVariant),
"rounded-full px-1.5 ml-1 text-xs h-5 leading-tight flex items-center justify-center"
)}
>
-{yearlyDiscount}% -{yearlyDiscount}%
</span> </span>
)} )}

View File

@@ -114,7 +114,7 @@ export const ModuleAnalyticsProgress = observer(function ModuleAnalyticsProgress
if (!moduleDetails) return <></>; if (!moduleDetails) return <></>;
return ( return (
<div className="border-t border-custom-border-200 space-y-4 py-4 px-3"> <div className="border-t border-custom-border-200 space-y-4 py-4">
<Disclosure defaultOpen={isModuleDateValid ? true : false}> <Disclosure defaultOpen={isModuleDateValid ? true : false}>
{({ open }) => ( {({ open }) => (
<div className="space-y-6"> <div className="space-y-6">

View File

@@ -1,7 +1,7 @@
import type { FC } from "react"; import type { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import type { TWorkItemFilterCondition } from "@plane/shared-state"; import type { TWorkItemFilterCondition } from "@plane/shared-state";
import type { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types"; import type { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils"; import { cn, toFilterArray } from "@plane/utils";
@@ -52,8 +52,6 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
`module-analytics-tab-${moduleId}`, `module-analytics-tab-${moduleId}`,
"stat-assignees" "stat-assignees"
); );
// derived values
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TModuleDistribution; const currentDistribution = distribution as TModuleDistribution;
const currentEstimateDistribution = distribution as TModuleEstimateDistribution; const currentEstimateDistribution = distribution as TModuleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[]; const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
@@ -114,9 +112,8 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
return ( return (
<div> <div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}> <Tabs defaultValue={currentTab ?? "stat-assignees"} onValueChange={(value) => setModuleTab(value)}>
<Tab.List <Tabs.List
as="div"
className={cn( className={cn(
`flex w-full items-center justify-between gap-2 rounded-md p-1`, `flex w-full items-center justify-between gap-2 rounded-md p-1`,
roundedTab ? `rounded-3xl` : `rounded-md`, roundedTab ? `rounded-3xl` : `rounded-md`,
@@ -125,39 +122,38 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
)} )}
> >
{PROGRESS_STATS.map((stat) => ( {PROGRESS_STATS.map((stat) => (
<Tab <Tabs.Trigger
key={stat.key}
value={stat.key}
className={cn( className={cn(
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`, `p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`, roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`,
stat.key === currentTab "data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-300",
? "bg-custom-background-100 text-custom-text-300" "text-custom-text-400 hover:text-custom-text-300"
: "text-custom-text-400 hover:text-custom-text-300"
)} )}
key={stat.key}
onClick={() => setModuleTab(stat.key)}
> >
{t(stat.i18n_title)} {t(stat.i18n_title)}
</Tab> </Tabs.Trigger>
))} ))}
</Tab.List> </Tabs.List>
<Tab.Panels className="py-3 text-custom-text-200"> <div className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-assignees"}> <Tabs.Content value="stat-assignees">
<AssigneeStatComponent <AssigneeStatComponent
distribution={distributionAssigneeData} distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate} handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable} isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds} selectedAssigneeIds={selectedAssigneeIds}
/> />
</Tab.Panel> </Tabs.Content>
<Tab.Panel key={"stat-labels"}> <Tabs.Content value="stat-labels">
<LabelStatComponent <LabelStatComponent
distribution={distributionLabelData} distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate} handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable} isEditable={isEditable}
selectedLabelIds={selectedLabelIds} selectedLabelIds={selectedLabelIds}
/> />
</Tab.Panel> </Tabs.Content>
<Tab.Panel key={"stat-states"}> <Tabs.Content value="stat-states">
<StateGroupStatComponent <StateGroupStatComponent
distribution={distributionStateData} distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate} handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
@@ -165,9 +161,9 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
selectedStateGroups={selectedStateGroups} selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount} totalIssuesCount={totalIssuesCount}
/> />
</Tab.Panel> </Tabs.Content>
</Tab.Panels> </div>
</Tab.Group> </Tabs>
</div> </div>
); );
}); });

View File

@@ -2,9 +2,9 @@ import React, { useCallback } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation"; import { useRouter, useSearchParams } from "next/navigation";
import { ArrowRightCircle } from "lucide-react"; import { ArrowRightCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import { Tooltip } from "@plane/propel/tooltip"; import { Tooltip } from "@plane/propel/tooltip";
// hooks // hooks
import { useQueryParams } from "@/hooks/use-query-params"; import { useQueryParams } from "@/hooks/use-query-params";
@@ -20,7 +20,6 @@ import { PageNavigationPaneTabsList } from "./tabs-list";
import type { INavigationPaneExtension } from "./types/extensions"; import type { INavigationPaneExtension } from "./types/extensions";
import { import {
PAGE_NAVIGATION_PANE_TAB_KEYS,
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM, PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM, PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
PAGE_NAVIGATION_PANE_WIDTH, PAGE_NAVIGATION_PANE_WIDTH,
@@ -49,7 +48,6 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
) as TPageNavigationPaneTab | null; ) as TPageNavigationPaneTab | null;
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline"; const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab);
// Check if any extension is currently active based on query parameters // Check if any extension is currently active based on query parameters
const ActiveExtension = extensions.find(function ActiveExtension(extension) { const ActiveExtension = extensions.find(function ActiveExtension(extension) {
@@ -69,8 +67,8 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
const { t } = useTranslation(); const { t } = useTranslation();
const handleTabChange = useCallback( const handleTabChange = useCallback(
(index: number) => { (value: string) => {
const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index]; const updatedTab = value as TPageNavigationPaneTab;
const isUpdatedTabInfo = updatedTab === "info"; const isUpdatedTabInfo = updatedTab === "info";
const updatedRoute = updateQueryParams({ const updatedRoute = updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab }, paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
@@ -106,10 +104,10 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
{ActiveExtension ? ( {ActiveExtension ? (
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} /> <ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
) : showNavigationTabs ? ( ) : showNavigationTabs ? (
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}> <Tabs value={activeTab} onValueChange={handleTabChange}>
<PageNavigationPaneTabsList /> <PageNavigationPaneTabsList />
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} /> <PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
</Tab.Group> </Tabs>
) : null} ) : null}
</div> </div>
</aside> </aside>

View File

@@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Tab } from "@headlessui/react"; // plane imports
import { Tabs } from "@plane/propel/tabs";
// components // components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root"; import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// plane web imports // plane web imports
@@ -21,19 +22,19 @@ export function PageNavigationPaneTabPanelsRoot(props: Props) {
const { page, versionHistory } = props; const { page, versionHistory } = props;
return ( return (
<Tab.Panels as={React.Fragment}> <>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab.Panel <Tabs.Content
key={tab.key} key={tab.key}
as="div" value={tab.key}
className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none" className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none"
> >
{tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />} {tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />} {tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />} {tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
<PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} page={page} /> <PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} page={page} />
</Tab.Panel> </Tabs.Content>
))} ))}
</Tab.Panels> </>
); );
} }

View File

@@ -1,6 +1,6 @@
import { Tab } from "@headlessui/react";
// plane imports // plane imports
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
// plane web components // plane web components
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
@@ -9,29 +9,19 @@ export function PageNavigationPaneTabsList() {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Tab.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80 mx-3.5"> <div className="mx-3.5">
{({ selectedIndex }) => ( <Tabs.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80">
<> {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( <Tabs.Trigger
<Tab key={tab.key}
key={tab.key} value={tab.key}
type="button" className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none" >
> {t(tab.i18n_label)}
{t(tab.i18n_label)} </Tabs.Trigger>
</Tab> ))}
))} <Tabs.Indicator className="absolute top-1/2 -translate-y-1/2 bg-custom-background-90 rounded transition-all duration-500 ease-in-out pointer-events-none" />
{/* active tab indicator */} </Tabs.List>
<div </div>
className="absolute top-1/2 -translate-y-1/2 bg-custom-background-90 rounded transition-all duration-500 ease-in-out pointer-events-none"
style={{
left: `calc(${(selectedIndex / ORDERED_PAGE_NAVIGATION_TABS_LIST.length) * 100}% + 2px)`,
height: "calc(100% - 4px)",
width: `calc(${100 / ORDERED_PAGE_NAVIGATION_TABS_LIST.length}% - 4px)`,
}}
/>
</>
)}
</Tab.List>
); );
} }

View File

@@ -25,7 +25,6 @@ export * from "./scroll-area";
export * from "./sortable"; export * from "./sortable";
export * from "./spinners"; export * from "./spinners";
export * from "./tables"; export * from "./tables";
export * from "./tabs";
export * from "./tag"; export * from "./tag";
export * from "./tooltip"; export * from "./tooltip";
export * from "./typography"; export * from "./typography";

View File

@@ -1,2 +0,0 @@
export * from "./tabs";
export * from "./tab-list";

View File

@@ -1,74 +0,0 @@
import { Tab } from "@headlessui/react";
import type { LucideProps } from "lucide-react";
import type { FC } from "react";
import React from "react";
// helpers
import { cn } from "../utils";
export type TabListItem = {
key: string;
icon?: FC<LucideProps>;
label?: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
};
type TTabListProps = {
tabs: TabListItem[];
tabListClassName?: string;
tabClassName?: string;
size?: "sm" | "md" | "lg";
selectedTab?: string;
onTabChange?: (key: string) => void;
};
export function TabList({
tabs,
tabListClassName,
tabClassName,
size = "md",
selectedTab,
onTabChange,
}: TTabListProps) {
return (
<Tab.List
as="div"
className={cn(
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md text-sm p-0.5 bg-custom-background-80/60",
tabListClassName
)}
>
{tabs.map((tab) => (
<Tab
className={({ selected }) =>
cn(
"flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded",
(selectedTab ? selectedTab === tab.key : selected)
? "bg-custom-background-100 text-custom-text-100 shadow-sm"
: tab.disabled
? "text-custom-text-400 cursor-not-allowed"
: "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60",
{
"text-xs": size === "sm",
"text-sm": size === "md",
"text-base": size === "lg",
},
tabClassName
)
}
key={tab.key}
onClick={() => {
if (!tab.disabled) {
onTabChange?.(tab.key);
tab.onClick?.();
}
}}
disabled={tab.disabled}
>
{tab.icon && <tab.icon className="size-4" />}
{tab.label}
</Tab>
))}
</Tab.List>
);
}

View File

@@ -1,90 +0,0 @@
import { Tab } from "@headlessui/react";
import type { FC } from "react";
import React, { Fragment, useEffect, useState } from "react";
// helpers
import { useLocalStorage } from "@plane/hooks";
import { cn } from "../utils";
// types
import type { TabListItem } from "./tab-list";
import { TabList } from "./tab-list";
export type TabContent = {
content: React.ReactNode;
};
export type TabItem = TabListItem & TabContent;
type TTabsProps = {
tabs: TabItem[];
storageKey?: string;
actions?: React.ReactNode;
defaultTab?: string;
containerClassName?: string;
tabListContainerClassName?: string;
tabListClassName?: string;
tabClassName?: string;
tabPanelClassName?: string;
size?: "sm" | "md" | "lg";
storeInLocalStorage?: boolean;
};
export function Tabs(props: TTabsProps) {
const {
tabs,
storageKey,
actions,
defaultTab = tabs[0]?.key,
containerClassName = "",
tabListContainerClassName = "",
tabListClassName = "",
tabClassName = "",
tabPanelClassName = "",
size = "md",
storeInLocalStorage = true,
} = props;
// local storage
const { storedValue, setValue } = useLocalStorage(
storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
defaultTab
);
// state
const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab);
useEffect(() => {
if (storeInLocalStorage) {
setValue(selectedTab);
}
}, [selectedTab, setValue, storeInLocalStorage, storageKey]);
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
const handleTabChange = (key: string) => {
setSelectedTab(key);
};
return (
<div className="flex flex-col w-full h-full">
<Tab.Group defaultIndex={currentTabIndex(selectedTab)}>
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
<TabList
tabs={tabs}
tabListClassName={tabListClassName}
tabClassName={tabClassName}
size={size}
onTabChange={handleTabChange}
/>
{actions && <div className="flex-grow">{actions}</div>}
</div>
<Tab.Panels as={Fragment}>
{tabs.map((tab) => (
<Tab.Panel key={tab.key} as="div" className={cn("relative outline-none", tabPanelClassName)}>
{tab.content}
</Tab.Panel>
))}
</Tab.Panels>
</div>
</Tab.Group>
</div>
);
}