From 813679d21f136dda8e4dd10e118f11fa4eab0fb5 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 20 Aug 2025 01:01:44 +0530 Subject: [PATCH 01/16] refactor: type safety in tabs --- .../(projects)/analytics/[tabId]/page.tsx | 5 +- .../analytics/work-items/modal/content.tsx | 15 +++--- .../analytics-sidebar/progress-stats.tsx | 54 ++++++++++++++++--- packages/ui/src/tabs/tab-list.tsx | 22 +++----- packages/ui/src/tabs/tabs.tsx | 10 ++-- 5 files changed, 68 insertions(+), 38 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index f75edf89e5c..d6cc2cfbe2f 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -6,6 +6,7 @@ import { useRouter } from "next/navigation"; // plane package imports import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; +import { TAnalyticsTabsBase } from "@plane/types"; import { type TabItem, Tabs } from "@plane/ui"; // components import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions"; @@ -59,7 +60,7 @@ const AnalyticsPage = observer((props: Props) => { ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) : undefined; const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]); - const tabs: TabItem[] = useMemo( + const tabs: TabItem[] = useMemo( () => ANALYTICS_TABS.map((tab) => ({ key: tab.key, @@ -72,7 +73,7 @@ const AnalyticsPage = observer((props: Props) => { })), [ANALYTICS_TABS, router, currentWorkspace?.slug] ); - const defaultTab = tabId || ANALYTICS_TABS[0].key; + const defaultTab = (tabId as TAnalyticsTabsBase) || ANALYTICS_TABS[0].key; return ( <> diff --git a/apps/web/core/components/analytics/work-items/modal/content.tsx b/apps/web/core/components/analytics/work-items/modal/content.tsx index 84188bbed63..ce2acdf1bb6 100644 --- a/apps/web/core/components/analytics/work-items/modal/content.tsx +++ b/apps/web/core/components/analytics/work-items/modal/content.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; -import { Tab } from "@headlessui/react"; // plane package imports import { ICycle, IModule, IProject } from "@plane/types"; import { Spinner } from "@plane/ui"; @@ -68,13 +67,11 @@ export const WorkItemsModalMainContent: React.FC = observer((props) => { ); return ( - -
- - - - -
-
+
+ + + + +
); }); diff --git a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index b0de0ce2401..3c20d45092d 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -3,8 +3,6 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; -import { Tab } from "@headlessui/react"; -// plane imports import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions, @@ -14,7 +12,7 @@ import { TCyclePlotType, TStateGroups, } from "@plane/types"; -import { Avatar, StateGroupIcon } from "@plane/ui"; +import { Avatar, StateGroupIcon, TabItem, Tabs } from "@plane/ui"; import { cn, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats"; @@ -250,6 +248,9 @@ type TCycleProgressStats = { noBackground?: boolean; }; +type TCycleProgressStatsTabKey = "stat-states" | "stat-assignees" | "stat-labels"; +type TCycleProgressStatsTabs = TabItem[]; + export const CycleProgressStats: FC = observer((props) => { const { cycleId, @@ -265,13 +266,12 @@ export const CycleProgressStats: FC = observer((props) => { noBackground = false, } = props; // hooks - const { storedValue: currentTab, setValue: setCycleTab } = useLocalStorage( + const { storedValue: currentTab, setValue: setCycleTab } = useLocalStorage( `cycle-analytics-tab-${cycleId}`, "stat-assignees" ); const { t } = useTranslation(); // derived values - const currentTabIndex = (tab: string): number => progressStats.findIndex((stat) => stat.key === tab); const currentDistribution = distribution as TCycleDistribution; const currentEstimateDistribution = distribution as TCycleEstimateDistribution; @@ -316,9 +316,48 @@ export const CycleProgressStats: FC = observer((props) => { total: totalIssuesCount || 0, })); + const cycleProgressStatsTabs: TCycleProgressStatsTabs = [ + { + key: "stat-states", + label: t("common.states"), + content: ( + + ), + }, + { + key: "stat-assignees", + label: t("common.assignees"), + content: ( + + ), + }, + { + key: "stat-labels", + label: t("common.labels"), + content: ( + + ), + }, + ]; + return (
- + {/* = observer((props) => { /> - + */} +
); }); diff --git a/packages/ui/src/tabs/tab-list.tsx b/packages/ui/src/tabs/tab-list.tsx index c60ab1fa775..0f1b1a0e0e1 100644 --- a/packages/ui/src/tabs/tab-list.tsx +++ b/packages/ui/src/tabs/tab-list.tsx @@ -4,33 +4,25 @@ import React, { FC } from "react"; // helpers import { cn } from "../utils"; -export type TabListItem = { - key: string; +export type TabListItem = { + key: TKey; icon?: FC; label?: React.ReactNode; disabled?: boolean; onClick?: () => void; }; -type TTabListProps = { - tabs: TabListItem[]; +type TTabListProps = { + tabs: TabListItem[]; tabListClassName?: string; tabClassName?: string; size?: "sm" | "md" | "lg"; - selectedTab?: string; - onTabChange?: (key: string) => void; + selectedTab?: TKey; + onTabChange?: (key: TKey) => void; }; -export const TabList: FC = ({ - tabs, - tabListClassName, - tabClassName, - size = "md", - selectedTab, - onTabChange, -}) => ( +export const TabList = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab, onTabChange }: TTabListProps) => ( = TabListItem & TabContent; -type TTabsProps = { - tabs: TabItem[]; +type TTabsProps[]> = { + tabs: TTabs; storageKey?: string; actions?: React.ReactNode; - defaultTab?: string; + defaultTab?: TTabs[number]["key"]; containerClassName?: string; tabListContainerClassName?: string; tabListClassName?: string; @@ -26,7 +26,7 @@ type TTabsProps = { storeInLocalStorage?: boolean; }; -export const Tabs: FC = (props: TTabsProps) => { +export const Tabs = []>(props: TTabsProps) => { const { tabs, storageKey, From 3e571d4e4b27aca19b051aefbd5e3e15db34cad4 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:13:50 +0530 Subject: [PATCH 02/16] refactor: improve type definitions for TabListItem and TabItem --- packages/ui/src/tabs/tab-list.tsx | 2 +- packages/ui/src/tabs/tabs.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/tabs/tab-list.tsx b/packages/ui/src/tabs/tab-list.tsx index 0f1b1a0e0e1..a6a7a33bf94 100644 --- a/packages/ui/src/tabs/tab-list.tsx +++ b/packages/ui/src/tabs/tab-list.tsx @@ -4,7 +4,7 @@ import React, { FC } from "react"; // helpers import { cn } from "../utils"; -export type TabListItem = { +export type TabListItem = { key: TKey; icon?: FC; label?: React.ReactNode; diff --git a/packages/ui/src/tabs/tabs.tsx b/packages/ui/src/tabs/tabs.tsx index 246d2680c01..40d8a2a7f6e 100644 --- a/packages/ui/src/tabs/tabs.tsx +++ b/packages/ui/src/tabs/tabs.tsx @@ -10,7 +10,7 @@ export type TabContent = { content: React.ReactNode; }; -export type TabItem = TabListItem & TabContent; +export type TabItem = TabListItem & TabContent; type TTabsProps[]> = { tabs: TTabs; From c0fb81271e00ecd147f7d870706b15c462762b3f Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 20 Aug 2025 18:24:21 +0530 Subject: [PATCH 03/16] refacator: improved types of tabs and migrated some components to use propel/tabs --- .../cycles/active-cycle/cycle-stats.tsx | 454 ++++++++---------- .../analytics-sidebar/progress-stats.tsx | 3 +- .../modal/card/base-paid-plan-card.tsx | 108 ++--- .../analytics-sidebar/progress-stats.tsx | 125 ++--- packages/propel/package.json | 6 +- packages/propel/src/tabs/index.ts | 2 + packages/propel/src/tabs/tab-list.tsx | 64 +++ packages/propel/src/tabs/tabs.tsx | 19 +- 8 files changed, 379 insertions(+), 402 deletions(-) create mode 100644 packages/propel/src/tabs/tab-list.tsx diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx index 2ecb846dea9..bf6009a34e7 100644 --- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -4,14 +4,13 @@ import { FC, Fragment, useCallback, useRef, useState } from "react"; import isEmpty from "lodash/isEmpty"; import { observer } from "mobx-react"; import { CalendarCheck } from "lucide-react"; -// headless ui -import { Tab } from "@headlessui/react"; // plane imports import { useTranslation } from "@plane/i18n"; +import { Tabs, TabItem } from "@plane/propel/tabs"; import { EIssuesStoreType, ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; -import { cn, renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@plane/utils"; +import { renderFormattedDate, renderFormattedDateWithoutYear, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats"; import { StateDropdown } from "@/components/dropdowns/state/dropdown"; @@ -37,10 +36,12 @@ export type ActiveCycleStatsProps = { cycleIssueDetails: ActiveCycleIssueDetails; }; +export type TActiveCycleStatsTab = "Priority-Issues" | "Assignees" | "Labels"; + export const ActiveCycleStats: FC = observer((props) => { const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props; // local storage - const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees"); + const { storedValue: tab } = useLocalStorage("activeCycleTab", "Assignees"); // refs const issuesContainerRef = useRef(null); // states @@ -52,18 +53,6 @@ export const ActiveCycleStats: FC = observer((props) => { const assigneesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/assignee" }); const labelsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/label" }); - const currentValue = (tab: string | null) => { - switch (tab) { - case "Priority-Issues": - return 0; - case "Assignees": - return 1; - case "Labels": - return 2; - default: - return 0; - } - }; const { issues: { fetchNextActiveCycleIssues }, } = useIssues(EIssuesStoreType.CYCLE); @@ -71,6 +60,7 @@ export const ActiveCycleStats: FC = observer((props) => { issue: { getIssueById }, setPeekIssue, } = useIssueDetail(); + const loadMoreIssues = useCallback(() => { if (!cycleId) return; fetchNextActiveCycleIssues(workspaceSlug, projectId, cycleId); @@ -87,271 +77,211 @@ export const ActiveCycleStats: FC = observer((props) => { ); - return cycleId ? ( -
- { - switch (i) { - case 0: - return setTab("Priority-Issues"); - case 1: - return setTab("Assignees"); - case 2: - return setTab("Labels"); - - default: - return setTab("Priority-Issues"); - } - }} - > - - - cn( - "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", - { - "text-custom-text-300 bg-custom-background-100": selected, - "hover:text-custom-text-300": !selected, - } - ) - } - > - {t("project_cycles.active_cycle.priority_issue")} - - - cn( - "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", - { - "text-custom-text-300 bg-custom-background-100": selected, - "hover:text-custom-text-300": !selected, - } - ) - } + // improvement: create tabs configuration map for better maintainability + const cycleStatsTabs: TabItem[] = [ + { + key: "Priority-Issues", + label: t("project_cycles.active_cycle.priority_issue"), + content: ( +
+
- {t("project_cycles.active_cycle.assignees")} - - - cn( - "relative z-[1] font-semibold text-xs rounded-[3px] py-1.5 text-custom-text-400 focus:outline-none transition duration-500", - { - "text-custom-text-300 bg-custom-background-100": selected, - "hover:text-custom-text-300": !selected, - } - ) - } - > - {t("project_cycles.active_cycle.labels")} - - - - - -
- {cycleIssueDetails && "issueIds" in cycleIssueDetails ? ( - cycleIssueDetails.issueCount > 0 ? ( - <> - {cycleIssueDetails.issueIds.map((issueId: string) => { - const issue = getIssueById(issueId); + {cycleIssueDetails && "issueIds" in cycleIssueDetails ? ( + cycleIssueDetails.issueCount > 0 ? ( + <> + {cycleIssueDetails.issueIds.map((issueId: string) => { + const issue = getIssueById(issueId); - if (!issue) return null; + if (!issue) return null; - return ( -
{ - if (issue.id) { - setPeekIssue({ - workspaceSlug, - projectId, - issueId: issue.id, - isArchived: !!issue.archived_at, - }); - handleFiltersUpdate("priority", ["urgent", "high"], true); - } - }} - > -
- - - {issue.name} - -
- -
- {}} - projectId={projectId?.toString() ?? ""} - disabled - buttonVariant="background-with-text" - buttonContainerClassName="cursor-pointer max-w-24" - showTooltip - /> - {issue.target_date && ( - -
- - - {renderFormattedDateWithoutYear(issue.target_date)} - -
-
- )} -
-
- ); - })} - {(cycleIssueDetails.nextPageResults === undefined || cycleIssueDetails.nextPageResults) && ( -
- )} - - ) : ( -
- -
- ) - ) : ( - loaders - )} -
- - - - {cycle && !isEmpty(cycle.distribution) ? ( - cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? ( - cycle.distribution?.assignees?.map((assignee, index) => { - if (assignee.assignee_id) return ( - - - - {assignee.display_name} -
- } - completed={assignee.completed_issues} - total={assignee.total_issues} +
{ - if (assignee.assignee_id) { - handleFiltersUpdate("assignees", [assignee.assignee_id], true); + if (issue.id) { + setPeekIssue({ + workspaceSlug, + projectId, + issueId: issue.id, + isArchived: !!issue.archived_at, + }); + handleFiltersUpdate("priority", ["urgent", "high"], true); } }} - /> - ); - else - return ( - -
- User -
- {t("no_assignee")} -
- } - completed={assignee.completed_issues} - total={assignee.total_issues} - /> + > +
+ + + {issue.name} + +
+ +
+ {}} + projectId={projectId?.toString() ?? ""} + disabled + buttonVariant="background-with-text" + buttonContainerClassName="cursor-pointer max-w-24" + showTooltip + /> + {issue.target_date && ( + +
+ + + {renderFormattedDateWithoutYear(issue.target_date)} + +
+
+ )} +
+
); - }) + })} + {(cycleIssueDetails.nextPageResults === undefined || cycleIssueDetails.nextPageResults) && ( +
+ )} + ) : (
) ) : ( loaders )} - +
+
+ ), + }, + { + key: "Assignees", + label: t("project_cycles.active_cycle.assignees"), + content: ( +
+ {cycle && !isEmpty(cycle.distribution) ? ( + cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? ( + cycle.distribution?.assignees?.map((assignee, index) => { + if (assignee.assignee_id) + return ( + + - - {cycle && !isEmpty(cycle.distribution) ? ( - cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? ( - cycle.distribution.labels?.map((label, index) => ( - - - {label.label_name ?? "No labels"} -
- } - completed={label.completed_issues} - total={label.total_issues} - onClick={() => { - if (label.label_id) { - handleFiltersUpdate("labels", [label.label_id], true); + {assignee.display_name} +
} - }} - /> - )) - ) : ( -
- -
- ) + completed={assignee.completed_issues} + total={assignee.total_issues} + onClick={() => { + if (assignee.assignee_id) { + handleFiltersUpdate("assignees", [assignee.assignee_id], true); + } + }} + /> + ); + else + return ( + +
+ User +
+ {t("no_assignee")} + + } + completed={assignee.completed_issues} + total={assignee.total_issues} + /> + ); + }) ) : ( - loaders - )} - - - +
+ +
+ ) + ) : ( + loaders + )} + + ), + }, + { + key: "Labels", + label: t("project_cycles.active_cycle.labels"), + content: ( +
+ {cycle && !isEmpty(cycle.distribution) ? ( + cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? ( + cycle.distribution.labels?.map((label, index) => ( + + + {label.label_name ?? "No labels"} +
+ } + completed={label.completed_issues} + total={label.total_issues} + onClick={() => { + if (label.label_id) { + handleFiltersUpdate("labels", [label.label_id], true); + } + }} + /> + )) + ) : ( +
+ +
+ ) + ) : ( + loaders + )} + + ), + }, + ]; + + return cycleId ? ( +
+
- ) : ( - - - - ); + ) : null; }); diff --git a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index 3c20d45092d..2ecc28c17f2 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -4,6 +4,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTranslation } from "@plane/i18n"; +import { TabItem, Tabs } from "@plane/propel/tabs"; import { IIssueFilterOptions, IIssueFilters, @@ -12,7 +13,7 @@ import { TCyclePlotType, TStateGroups, } from "@plane/types"; -import { Avatar, StateGroupIcon, TabItem, Tabs } from "@plane/ui"; +import { Avatar, StateGroupIcon } from "@plane/ui"; import { cn, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats"; diff --git a/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx b/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx index 746aea24ad7..fb10a24ce55 100644 --- a/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx +++ b/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx @@ -1,10 +1,10 @@ "use client"; -import { FC, useState } from "react"; +import { FC, ReactNode, useState } from "react"; import { observer } from "mobx-react"; import { CheckCircle } from "lucide-react"; -import { Tab } from "@headlessui/react"; // plane imports +import { Tabs, TabItem } from "@plane/propel/tabs"; // helpers import { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types"; import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui"; @@ -40,61 +40,61 @@ export const BasePaidPlanCard: FC = observer((props) => // Plane details const planeName = getSubscriptionName(planVariant); + // improvement: create tabs configuration map for better maintainability + const billingFrequencyTabs: TabItem[] = prices.map((price: TSubscriptionPrice) => ({ + key: price.recurring, + label: renderPriceContent(price), + content: ( +
+
Plane {planeName}
+ {renderActionButton(price)} +
+ ), + onClick: () => setSelectedPlan(price.recurring), + })); + return (
- -
- - {prices.map((price: TSubscriptionPrice) => ( - - cn( - "w-full rounded py-1 text-sm font-medium leading-5", - selected - ? "bg-custom-background-100 text-custom-text-100 shadow" - : "text-custom-text-300 hover:text-custom-text-200" - ) - } - onClick={() => setSelectedPlan(price.recurring)} - > - {renderPriceContent(price)} - - ))} - -
- - {prices.map((price: TSubscriptionPrice) => ( - -
-
Plane {planeName}
- {renderActionButton(price)} -
-
-
{`Everything in ${basePlan} +`}
-
    - {features.map((feature) => ( -
  • -

    - - {feature} -

    -
  • - ))} -
- {extraFeatures &&
{extraFeatures}
} -
-
+
+ +
+ + {/* Features section - rendered outside tabs since it's common for all billing frequencies */} +
+
{`Everything in ${basePlan} +`}
+
    + {features.map((feature) => ( +
  • +

    + + {feature} +

    +
  • ))} - - +
+ {extraFeatures &&
{extraFeatures}
} +
); }); diff --git a/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx index 555e7e32bb2..c2dfbb318c5 100644 --- a/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx @@ -3,8 +3,8 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; -import { Tab } from "@headlessui/react"; import { useTranslation } from "@plane/i18n"; +import { Tabs, TabItem } from "@plane/propel/tabs"; import { IIssueFilterOptions, IIssueFilters, @@ -14,7 +14,8 @@ import { TStateGroups, } from "@plane/types"; import { Avatar, StateGroupIcon } from "@plane/ui"; -import { cn, getFileURL } from "@plane/utils"; + +import { getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats"; // helpers @@ -220,21 +221,7 @@ export const StateStatComponent = observer((props: TStateStatComponent) => { ); }); -const progressStats = [ - { - key: "stat-assignees", - title: "Assignees", - }, - { - key: "stat-labels", - title: "Labels", - }, - { - key: "stat-states", - title: "States", - }, -]; - +export type TModuleProgressStatsTab = "stat-assignees" | "stat-labels" | "stat-states"; type TModuleProgressStats = { moduleId: string; plotType: TModulePlotType; @@ -264,12 +251,10 @@ export const ModuleProgressStats: FC = observer((props) => noBackground = false, } = props; // hooks - const { storedValue: currentTab, setValue: setModuleTab } = useLocalStorage( + const { storedValue: currentTab } = useLocalStorage( `module-analytics-tab-${moduleId}`, "stat-assignees" ); - // derived values - const currentTabIndex = (tab: string): number => progressStats.findIndex((stat) => stat.key === tab); const currentDistribution = distribution as TModuleDistribution; const currentEstimateDistribution = distribution as TModuleEstimateDistribution; @@ -314,61 +299,55 @@ export const ModuleProgressStats: FC = observer((props) => total: totalIssuesCount || 0, })); + // improvement: create tabs configuration map for better maintainability + const moduleProgressStatsTabs: TabItem[] = [ + { + key: "stat-assignees", + label: "Assignees", + content: ( + + ), + }, + { + key: "stat-labels", + label: "Labels", + content: ( + + ), + }, + { + key: "stat-states", + label: "States", + content: ( + + ), + }, + ]; + return (
- - - {progressStats.map((stat) => ( - setModuleTab(stat.key)} - > - {stat.title} - - ))} - - - - - - - - - - - - - +
); }); diff --git a/packages/propel/package.json b/packages/propel/package.json index 11e6921eb6a..0bf3b7ce451 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -42,8 +42,8 @@ "@plane/eslint-config": "workspace:*", "@plane/tailwind-config": "workspace:*", "@plane/typescript-config": "workspace:*", - "@types/react": "18.3.1", - "@types/react-dom": "18.3.0", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.2.18", "typescript": "5.8.3" } -} +} \ No newline at end of file diff --git a/packages/propel/src/tabs/index.ts b/packages/propel/src/tabs/index.ts index e69de29bb2d..c232b43ab42 100644 --- a/packages/propel/src/tabs/index.ts +++ b/packages/propel/src/tabs/index.ts @@ -0,0 +1,2 @@ +export * from "./tabs"; +export * from "./tab-list"; diff --git a/packages/propel/src/tabs/tab-list.tsx b/packages/propel/src/tabs/tab-list.tsx new file mode 100644 index 00000000000..734602ac7b2 --- /dev/null +++ b/packages/propel/src/tabs/tab-list.tsx @@ -0,0 +1,64 @@ +import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; +import { LucideProps } from "lucide-react"; +import React, { FC } from "react"; +import { cn } from "@plane/utils"; +// helpers + +export type TabListItem = { + key: TKey; + icon?: FC; + label?: React.ReactNode; + disabled?: boolean; + onClick?: () => void; +}; + +type TTabListProps = { + tabs: TabListItem[]; + tabListClassName?: string; + tabClassName?: string; + size?: "sm" | "md" | "lg"; + selectedTab?: TKey; +}; + +export const TabList = ({ + tabs, + tabListClassName, + tabClassName, + size = "md", + selectedTab, +}: TTabListProps) => ( + + {tabs.map((tab) => ( + + 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} + disabled={tab.disabled} + > + {tab.icon && } + {tab.label} + + ))} + + + +); diff --git a/packages/propel/src/tabs/tabs.tsx b/packages/propel/src/tabs/tabs.tsx index ad40370cf33..e7693b61e86 100644 --- a/packages/propel/src/tabs/tabs.tsx +++ b/packages/propel/src/tabs/tabs.tsx @@ -1,22 +1,23 @@ -import React, { FC, useEffect, useState } from "react"; import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; +import React, { FC, useEffect, useState } from "react"; // helpers import { useLocalStorage } from "@plane/hooks"; -import { cn } from "@plane/utils"; + // types -import { TabList, TabListItem } from "./list"; +import { TabList, TabListItem } from "./tab-list"; +import { cn } from "@plane/utils"; export type TabContent = { content: React.ReactNode; }; -export type TabItem = TabListItem & TabContent; +export type TabItem = TabListItem & TabContent; -type TTabsProps = { - tabs: TabItem[]; +type TTabsProps[]> = { + tabs: TTabs; storageKey?: string; actions?: React.ReactNode; - defaultTab?: string; + defaultTab?: TTabs[number]["key"]; containerClassName?: string; tabListContainerClassName?: string; tabListClassName?: string; @@ -26,7 +27,7 @@ type TTabsProps = { storeInLocalStorage?: boolean; }; -export const Tabs: FC = (props: TTabsProps) => { +export const Tabs = []>(props: TTabsProps) => { const { tabs, storageKey, @@ -68,7 +69,7 @@ export const Tabs: FC = (props: TTabsProps) => {
Date: Wed, 20 Aug 2025 22:30:57 +0530 Subject: [PATCH 04/16] refactor: migrate components to use propel/tabs for improved tab functionality and type safety --- .../(projects)/analytics/[tabId]/page.tsx | 51 ++- .../components/core/image-picker-popover.tsx | 377 +++++++++--------- .../cycles/active-cycle/cycle-stats.tsx | 377 +++++++++--------- .../analytics-sidebar/issue-progress.tsx | 1 - .../analytics-sidebar/progress-stats.tsx | 155 +++---- .../modal/card/base-paid-plan-card.tsx | 52 +-- .../analytics-sidebar/issue-progress.tsx | 2 - .../analytics-sidebar/progress-stats.tsx | 97 +++-- .../components/pages/navigation-pane/root.tsx | 15 +- .../navigation-pane/tab-panels/assets.tsx | 12 +- .../pages/navigation-pane/tab-panels/root.tsx | 14 +- .../pages/navigation-pane/tabs-list.tsx | 33 +- packages/propel/src/tabs/index.ts | 1 - packages/propel/src/tabs/tab-list.tsx | 64 --- packages/propel/src/tabs/tabs.tsx | 137 +++---- packages/ui/src/tabs/composable-tabs.tsx | 59 +++ 16 files changed, 664 insertions(+), 783 deletions(-) delete mode 100644 packages/propel/src/tabs/tab-list.tsx create mode 100644 packages/ui/src/tabs/composable-tabs.tsx diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index d6cc2cfbe2f..4bbc48cce96 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -7,7 +7,7 @@ import { useRouter } from "next/navigation"; import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { TAnalyticsTabsBase } from "@plane/types"; -import { type TabItem, Tabs } from "@plane/ui"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; // components import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions"; import { PageHead } from "@/components/core/page-title"; @@ -60,40 +60,37 @@ const AnalyticsPage = observer((props: Props) => { ? t(`workspace_analytics.page_label`, { workspace: currentWorkspace?.name }) : undefined; const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]); - const tabs: TabItem[] = useMemo( - () => - ANALYTICS_TABS.map((tab) => ({ - key: tab.key, - label: tab.label, - content: , - onClick: () => { - router.push(`/${currentWorkspace?.slug}/analytics/${tab.key}`); - }, - disabled: tab.isDisabled, - })), - [ANALYTICS_TABS, router, currentWorkspace?.slug] - ); const defaultTab = (tabId as TAnalyticsTabsBase) || ANALYTICS_TABS[0].key; + const handleTabChange = (value: string) => { + router.push(`/${currentWorkspace?.slug}/analytics/${value}`); + }; + return ( <> {workspaceProjectIds && ( <> {workspaceProjectIds.length > 0 || loader === "init-loader" ? ( -
- } - /> +
+ +
+ + {ANALYTICS_TABS.map((tab) => ( + + {tab.label} + + ))} + + +
+ + {ANALYTICS_TABS.map((tab) => ( + + + + ))} +
) : ( = observer((props) => { >
- - + + {tabOptions.map((tab) => { if (!unsplashImages && unsplashError && tab.key === "unsplash") return null; if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images") return null; return ( - - `rounded px-4 py-1 text-center text-sm outline-none transition-colors ${ - selected ? "bg-custom-primary text-white" : "text-custom-text-100" - }` - } - > + {tab.title} - + ); })} - - - {(unsplashImages || !unsplashError) && ( - -
- ( - { - if (e.key === "Enter") { - e.preventDefault(); - setSearchParams(formData.search); - } + + + {(unsplashImages || !unsplashError) && ( + +
+ ( + { + if (e.key === "Enter") { + e.preventDefault(); + setSearchParams(formData.search); + } + }} + value={value} + onChange={(e) => setFormData({ ...formData, search: e.target.value })} + ref={ref} + placeholder="Search for images" + className="w-full text-sm" + /> + )} + /> + +
+ {unsplashImages ? ( + unsplashImages.length > 0 ? ( +
+ {unsplashImages.map((image) => ( +
{ + setIsOpen(false); + onChange(image.urls.regular); }} - value={value} - onChange={(e) => setFormData({ ...formData, search: e.target.value })} - ref={ref} - placeholder="Search for images" - className="w-full text-sm" - /> - )} - /> - -
- {unsplashImages ? ( - unsplashImages.length > 0 ? ( -
- {unsplashImages.map((image) => ( -
{ - setIsOpen(false); - onChange(image.urls.regular); - }} - > - {image.alt_description} -
- ))} -
- ) : ( -

No images found.

- ) - ) : ( - - - - - - - - - - - )} - - )} - {(!projectCoverImages || projectCoverImages.length !== 0) && ( - - {projectCoverImages ? ( - projectCoverImages.length > 0 ? ( -
- {projectCoverImages.map((image, index) => ( -
{ - setIsOpen(false); - onChange(image); - }} - > - {`Default -
- ))} -
- ) : ( -

No images found.

- ) + > + {image.alt_description} +
+ ))} +
) : ( - - - - - - - - - - - )} -
- )} - -
-
-
- - {image !== null || (value && value !== "") ? ( - <> - imageNo images found.

+ ) + ) : ( + + + + + + + + + + + )} + + )} + + {(!projectCoverImages || projectCoverImages.length !== 0) && ( + + {projectCoverImages ? ( + projectCoverImages.length > 0 ? ( +
+ {projectCoverImages.map((image, index) => ( +
{ + setIsOpen(false); + onChange(image); + }} + > + {`Project - - ) : ( -
- - {isDragActive ? "Drop image here to upload" : "Drag & drop image here"} -
- )} + ))} +
+ ) : ( +

No images found.

+ ) + ) : ( + + + + + + + + + + + )} + + )} - + +
+
+
+ +
+
+
+ + + +
+ + {isDragActive ? "Drop the file here" : "Drag & drop or click to upload"} + +
+ + {Object.keys(ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE).join(", ")} (Max{" "} + {MAX_FILE_SIZE / 1024 / 1024}MB) +
- {fileRejections.length > 0 && ( -

- {fileRejections[0].errors[0].code === "file-too-large" - ? "The image size cannot exceed 5 MB." - : "Please upload a file in a valid format."} -

- )} - -

File formats supported- .jpeg, .jpg, .png, .webp

- -
+
+ {image && ( +
+
+ Preview +
+
+

{image.name}

+

{(image.size / 1024 / 1024).toFixed(2)} MB

+
-
-
- - - + )} + {fileRejections.length > 0 && ( +
+
+ + + +
+
+

File rejected

+

{fileRejections[0].errors[0].message}

+
+
+ )} + {image && ( + + )} +
+
+
)} diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx index bf6009a34e7..80a9fcb8557 100644 --- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -6,7 +6,7 @@ import { observer } from "mobx-react"; import { CalendarCheck } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; -import { Tabs, TabItem } from "@plane/propel/tabs"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { EIssuesStoreType, ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; @@ -41,7 +41,7 @@ export type TActiveCycleStatsTab = "Priority-Issues" | "Assignees" | "Labels"; export const ActiveCycleStats: FC = observer((props) => { const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props; // local storage - const { storedValue: tab } = useLocalStorage("activeCycleTab", "Assignees"); + const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees"); // refs const issuesContainerRef = useRef(null); // states @@ -77,93 +77,167 @@ export const ActiveCycleStats: FC = observer((props) => { ); - // improvement: create tabs configuration map for better maintainability - const cycleStatsTabs: TabItem[] = [ - { - key: "Priority-Issues", - label: t("project_cycles.active_cycle.priority_issue"), - content: ( -
-
- {cycleIssueDetails && "issueIds" in cycleIssueDetails ? ( - cycleIssueDetails.issueCount > 0 ? ( - <> - {cycleIssueDetails.issueIds.map((issueId: string) => { - const issue = getIssueById(issueId); + const handleTabChange = (value: string) => { + setTab(value as TActiveCycleStatsTab); + }; - if (!issue) return null; + return cycleId ? ( +
+ + + + {t("project_cycles.active_cycle.priority_issue")} + + + {t("project_cycles.active_cycle.assignees")} + + + {t("project_cycles.active_cycle.labels")} + + - return ( + +
+
+ {cycleIssueDetails && "issueIds" in cycleIssueDetails ? ( + cycleIssueDetails.issueCount > 0 ? ( + <> + {cycleIssueDetails.issueIds.map((issueId: string) => { + const issue = getIssueById(issueId); + + if (!issue) return null; + + return ( +
{ + if (issue.id) { + setPeekIssue({ + workspaceSlug, + projectId, + issueId: issue.id, + isArchived: !!issue.archived_at, + }); + handleFiltersUpdate("priority", ["urgent", "high"], true); + } + }} + > +
+ + + {issue.name} + +
+ +
+ {}} + projectId={projectId?.toString() ?? ""} + disabled + buttonVariant="background-with-text" + buttonContainerClassName="cursor-pointer max-w-24" + showTooltip + /> + {issue.target_date && ( + +
+ + + {renderFormattedDateWithoutYear(issue.target_date)} + +
+
+ )} +
+
+ ); + })} + {(cycleIssueDetails.nextPageResults === undefined || cycleIssueDetails.nextPageResults) && (
+ )} + + ) : ( +
+ +
+ ) + ) : ( + loaders + )} +
+
+ + + +
+ {cycle && !isEmpty(cycle.distribution) ? ( + cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? ( + cycle.distribution?.assignees?.map((assignee, index) => { + if (assignee.assignee_id) + return ( + + + + {assignee.display_name} +
+ } + completed={assignee.completed_issues} + total={assignee.total_issues} onClick={() => { - if (issue.id) { - setPeekIssue({ - workspaceSlug, - projectId, - issueId: issue.id, - isArchived: !!issue.archived_at, - }); - handleFiltersUpdate("priority", ["urgent", "high"], true); + if (assignee.assignee_id) { + handleFiltersUpdate("assignees", [assignee.assignee_id], true); } }} - > -
- - - {issue.name} - -
- -
- {}} - projectId={projectId?.toString() ?? ""} - disabled - buttonVariant="background-with-text" - buttonContainerClassName="cursor-pointer max-w-24" - showTooltip - /> - {issue.target_date && ( - -
- - - {renderFormattedDateWithoutYear(issue.target_date)} - -
-
- )} -
-
+ /> ); - })} - {(cycleIssueDetails.nextPageResults === undefined || cycleIssueDetails.nextPageResults) && ( -
- )} - + else + return ( + +
+ User +
+ {t("no_assignee")} +
+ } + completed={assignee.completed_issues} + total={assignee.total_issues} + /> + ); + }) ) : (
) @@ -171,117 +245,46 @@ export const ActiveCycleStats: FC = observer((props) => { loaders )}
-
- ), - }, - { - key: "Assignees", - label: t("project_cycles.active_cycle.assignees"), - content: ( -
- {cycle && !isEmpty(cycle.distribution) ? ( - cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? ( - cycle.distribution?.assignees?.map((assignee, index) => { - if (assignee.assignee_id) - return ( - - + - {assignee.display_name} -
- } - completed={assignee.completed_issues} - total={assignee.total_issues} - onClick={() => { - if (assignee.assignee_id) { - handleFiltersUpdate("assignees", [assignee.assignee_id], true); - } - }} - /> - ); - else - return ( - -
- User -
- {t("no_assignee")} -
- } - completed={assignee.completed_issues} - total={assignee.total_issues} - /> - ); - }) - ) : ( -
- -
- ) - ) : ( - loaders - )} -
- ), - }, - { - key: "Labels", - label: t("project_cycles.active_cycle.labels"), - content: ( -
- {cycle && !isEmpty(cycle.distribution) ? ( - cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? ( - cycle.distribution.labels?.map((label, index) => ( - - - {label.label_name ?? "No labels"} -
- } - completed={label.completed_issues} - total={label.total_issues} - onClick={() => { - if (label.label_id) { - handleFiltersUpdate("labels", [label.label_id], true); + +
+ {cycle && !isEmpty(cycle.distribution) ? ( + cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? ( + cycle.distribution.labels?.map((label, index) => ( + + + {label.label_name ?? "No labels"} +
} - }} - /> - )) + completed={label.completed_issues} + total={label.total_issues} + onClick={() => { + if (label.label_id) { + handleFiltersUpdate("labels", [label.label_id], true); + } + }} + /> + )) + ) : ( +
+ +
+ ) ) : ( -
- -
- ) - ) : ( - loaders - )} -
- ), - }, - ]; - - return cycleId ? ( -
- + loaders + )} +
+ +
) : null; }); diff --git a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index 70ed49f4cfa..a84475cad2d 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -178,7 +178,6 @@ export const CycleAnalyticsProgress: FC = observer((pro totalIssuesCount={estimateType === "points" ? totalEstimatePoints || 0 : totalIssues || 0} isEditable={Boolean(!peekCycle)} size="xs" - roundedTab={false} noBackground={false} filters={issueFilters} handleFiltersUpdate={handleFiltersUpdate} diff --git a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index 2ecc28c17f2..8b341b863c7 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTranslation } from "@plane/i18n"; -import { TabItem, Tabs } from "@plane/propel/tabs"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { IIssueFilterOptions, IIssueFilters, @@ -220,21 +220,6 @@ export const StateStatComponent = observer((props: TStateStatComponent) => { ); }); -const progressStats = [ - { - key: "stat-states", - i18n_title: "common.states", - }, - { - key: "stat-assignees", - i18n_title: "common.assignees", - }, - { - key: "stat-labels", - i18n_title: "common.labels", - }, -]; - type TCycleProgressStats = { cycleId: string; plotType: TCyclePlotType; @@ -250,7 +235,6 @@ type TCycleProgressStats = { }; type TCycleProgressStatsTabKey = "stat-states" | "stat-assignees" | "stat-labels"; -type TCycleProgressStatsTabs = TabItem[]; export const CycleProgressStats: FC = observer((props) => { const { @@ -263,7 +247,7 @@ export const CycleProgressStats: FC = observer((props) => { filters, handleFiltersUpdate, size = "sm", - roundedTab = false, + noBackground = false, } = props; // hooks @@ -317,101 +301,52 @@ export const CycleProgressStats: FC = observer((props) => { total: totalIssuesCount || 0, })); - const cycleProgressStatsTabs: TCycleProgressStatsTabs = [ - { - key: "stat-states", - label: t("common.states"), - content: ( - - ), - }, - { - key: "stat-assignees", - label: t("common.assignees"), - content: ( - - ), - }, - { - key: "stat-labels", - label: t("common.labels"), - content: ( - - ), - }, - ]; + const handleTabChange = (value: string) => { + setCycleTab(value as TCycleProgressStatsTabKey); + }; return (
- {/* - - {progressStats.map((stat) => ( - setCycleTab(stat.key)} - > - {t(stat.i18n_title)} - - ))} - - - - - - - - - - - - - */} - + + + + {t("common.states")} + + + {t("common.assignees")} + + + {t("common.labels")} + + + + + + + + + + + + + + +
); }); diff --git a/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx b/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx index fb10a24ce55..16495e442d6 100644 --- a/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx +++ b/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx @@ -1,10 +1,10 @@ "use client"; -import { FC, ReactNode, useState } from "react"; +import { FC, useState } from "react"; import { observer } from "mobx-react"; import { CheckCircle } from "lucide-react"; // plane imports -import { Tabs, TabItem } from "@plane/propel/tabs"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; // helpers import { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types"; import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui"; @@ -40,39 +40,27 @@ export const BasePaidPlanCard: FC = observer((props) => // Plane details const planeName = getSubscriptionName(planVariant); - // improvement: create tabs configuration map for better maintainability - const billingFrequencyTabs: TabItem[] = prices.map((price: TSubscriptionPrice) => ({ - key: price.recurring, - label: renderPriceContent(price), - content: ( -
-
Plane {planeName}
- {renderActionButton(price)} -
- ), - onClick: () => setSelectedPlan(price.recurring), - })); - return (
- + + + {prices.map((price: TSubscriptionPrice) => ( + + {renderPriceContent(price)} + + ))} + + + {prices.map((price: TSubscriptionPrice) => ( + +
+
Plane {planeName}
+ {renderActionButton(price)} +
+
+ ))} +
{/* Features section - rendered outside tabs since it's common for all billing frequencies */} diff --git a/apps/web/core/components/modules/analytics-sidebar/issue-progress.tsx b/apps/web/core/components/modules/analytics-sidebar/issue-progress.tsx index 53bd3a60db3..70f1d21266d 100644 --- a/apps/web/core/components/modules/analytics-sidebar/issue-progress.tsx +++ b/apps/web/core/components/modules/analytics-sidebar/issue-progress.tsx @@ -241,8 +241,6 @@ export const ModuleAnalyticsProgress: FC = observer((p totalIssuesCount={plotType === "points" ? totalEstimatePoints || 0 : totalIssues || 0} isEditable={Boolean(!peekModule)} size="xs" - roundedTab={false} - noBackground={false} filters={issueFilters} handleFiltersUpdate={handleFiltersUpdate} /> diff --git a/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx index c2dfbb318c5..fb57f9f412a 100644 --- a/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx @@ -4,7 +4,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTranslation } from "@plane/i18n"; -import { Tabs, TabItem } from "@plane/propel/tabs"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { IIssueFilterOptions, IIssueFilters, @@ -15,7 +15,7 @@ import { } from "@plane/types"; import { Avatar, StateGroupIcon } from "@plane/ui"; -import { getFileURL } from "@plane/utils"; +import { cn, getFileURL } from "@plane/utils"; // components import { SingleProgressStats } from "@/components/core/sidebar/single-progress-stats"; // helpers @@ -247,11 +247,9 @@ export const ModuleProgressStats: FC = observer((props) => filters, handleFiltersUpdate, size = "sm", - roundedTab = false, - noBackground = false, } = props; // hooks - const { storedValue: currentTab } = useLocalStorage( + const { storedValue: currentTab, setValue: setModuleTab } = useLocalStorage( `module-analytics-tab-${moduleId}`, "stat-assignees" ); @@ -299,55 +297,52 @@ export const ModuleProgressStats: FC = observer((props) => total: totalIssuesCount || 0, })); - // improvement: create tabs configuration map for better maintainability - const moduleProgressStatsTabs: TabItem[] = [ - { - key: "stat-assignees", - label: "Assignees", - content: ( - - ), - }, - { - key: "stat-labels", - label: "Labels", - content: ( - - ), - }, - { - key: "stat-states", - label: "States", - content: ( - - ), - }, - ]; + const handleTabChange = (value: TModuleProgressStatsTab) => { + setModuleTab(value); + }; return (
- + + + + Assignees + + + Labels + + + States + + + + + + + + + + + + + + +
); }); diff --git a/apps/web/core/components/pages/navigation-pane/root.tsx b/apps/web/core/components/pages/navigation-pane/root.tsx index 688e45078f0..71dd927c028 100644 --- a/apps/web/core/components/pages/navigation-pane/root.tsx +++ b/apps/web/core/components/pages/navigation-pane/root.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useRouter, useSearchParams } from "next/navigation"; import { ArrowRightCircle } from "lucide-react"; -import { Tab } from "@headlessui/react"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; // plane imports import { useTranslation } from "@plane/i18n"; import { Tooltip } from "@plane/ui"; @@ -42,13 +42,12 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM ) as TPageNavigationPaneTab | null; const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline"; - const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab); // translation const { t } = useTranslation(); const handleTabChange = useCallback( - (index: number) => { - const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index]; + (value: string) => { + const updatedTab = value as TPageNavigationPaneTab; const isUpdatedTabInfo = updatedTab === "info"; const updatedRoute = updateQueryParams({ paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab }, @@ -79,10 +78,14 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => {
- + - + ); }); diff --git a/apps/web/core/components/pages/navigation-pane/tab-panels/assets.tsx b/apps/web/core/components/pages/navigation-pane/tab-panels/assets.tsx index f551f07c613..2da1bed8ed1 100644 --- a/apps/web/core/components/pages/navigation-pane/tab-panels/assets.tsx +++ b/apps/web/core/components/pages/navigation-pane/tab-panels/assets.tsx @@ -107,13 +107,13 @@ export const PageNavigationPaneAssetsTabPanel: React.FC = observer((props editor: { assetsList }, } = page; - if (assetsList.length === 0) return ; - return ( -
- {assetsList?.map((asset) => ( - - ))} +
+ {assetsList?.length === 0 ? ( + + ) : ( + assetsList?.map((asset) => ) + )}
); }); diff --git a/apps/web/core/components/pages/navigation-pane/tab-panels/root.tsx b/apps/web/core/components/pages/navigation-pane/tab-panels/root.tsx index 6c4b4f28c14..284215d7bee 100644 --- a/apps/web/core/components/pages/navigation-pane/tab-panels/root.tsx +++ b/apps/web/core/components/pages/navigation-pane/tab-panels/root.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Tab } from "@headlessui/react"; +import { TabsContent } from "@plane/propel/tabs"; // components import type { TPageRootHandlers } from "@/components/pages/editor/page-root"; // plane web imports @@ -21,19 +21,15 @@ export const PageNavigationPaneTabPanelsRoot: React.FC = (props) => { const { page, versionHistory } = props; return ( - + <> {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( - + {tab.key === "outline" && } {tab.key === "info" && } {tab.key === "assets" && } - + ))} - + ); }; diff --git a/apps/web/core/components/pages/navigation-pane/tabs-list.tsx b/apps/web/core/components/pages/navigation-pane/tabs-list.tsx index bf438321683..20acd35c009 100644 --- a/apps/web/core/components/pages/navigation-pane/tabs-list.tsx +++ b/apps/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -1,4 +1,4 @@ -import { Tab } from "@headlessui/react"; +import { TabsList, TabsTrigger } from "@plane/propel/tabs"; // plane imports import { useTranslation } from "@plane/i18n"; // plane web components @@ -9,29 +9,12 @@ export const PageNavigationPaneTabsList = () => { const { t } = useTranslation(); return ( - - {({ selectedIndex }) => ( - <> - {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( - - {t(tab.i18n_label)} - - ))} - {/* active tab indicator */} -
- - )} - + + {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( + + {t(tab.i18n_label)} + + ))} + ); }; diff --git a/packages/propel/src/tabs/index.ts b/packages/propel/src/tabs/index.ts index c232b43ab42..811d3d4a725 100644 --- a/packages/propel/src/tabs/index.ts +++ b/packages/propel/src/tabs/index.ts @@ -1,2 +1 @@ export * from "./tabs"; -export * from "./tab-list"; diff --git a/packages/propel/src/tabs/tab-list.tsx b/packages/propel/src/tabs/tab-list.tsx deleted file mode 100644 index 734602ac7b2..00000000000 --- a/packages/propel/src/tabs/tab-list.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; -import { LucideProps } from "lucide-react"; -import React, { FC } from "react"; -import { cn } from "@plane/utils"; -// helpers - -export type TabListItem = { - key: TKey; - icon?: FC; - label?: React.ReactNode; - disabled?: boolean; - onClick?: () => void; -}; - -type TTabListProps = { - tabs: TabListItem[]; - tabListClassName?: string; - tabClassName?: string; - size?: "sm" | "md" | "lg"; - selectedTab?: TKey; -}; - -export const TabList = ({ - tabs, - tabListClassName, - tabClassName, - size = "md", - selectedTab, -}: TTabListProps) => ( - - {tabs.map((tab) => ( - - 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} - disabled={tab.disabled} - > - {tab.icon && } - {tab.label} - - ))} - - - -); diff --git a/packages/propel/src/tabs/tabs.tsx b/packages/propel/src/tabs/tabs.tsx index e7693b61e86..19923491a07 100644 --- a/packages/propel/src/tabs/tabs.tsx +++ b/packages/propel/src/tabs/tabs.tsx @@ -1,92 +1,63 @@ -import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; -import React, { FC, useEffect, useState } from "react"; -// helpers -import { useLocalStorage } from "@plane/hooks"; - -// types -import { TabList, TabListItem } from "./tab-list"; +import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs"; +import * as React from "react"; import { cn } from "@plane/utils"; -export type TabContent = { - content: React.ReactNode; -}; - -export type TabItem = TabListItem & TabContent; - -type TTabsProps[]> = { - tabs: TTabs; - storageKey?: string; - actions?: React.ReactNode; - defaultTab?: TTabs[number]["key"]; - containerClassName?: string; - tabListContainerClassName?: string; - tabListClassName?: string; - tabClassName?: string; - tabPanelClassName?: string; - size?: "sm" | "md" | "lg"; - storeInLocalStorage?: boolean; -}; +function Tabs({ className, ...props }: React.ComponentProps) { + return ; +} -export const Tabs = []>(props: TTabsProps) => { - const { - tabs, - storageKey, - actions, - defaultTab = tabs[0]?.key, - containerClassName = "", - tabListContainerClassName = "", - tabListClassName = "", - tabClassName = "", - tabPanelClassName = "", - size = "md", - storeInLocalStorage = true, - } = props; - - const { storedValue, setValue } = useLocalStorage( - storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`, - defaultTab +function TabsList({ className, ...props }: React.ComponentProps) { + return ( + ); +} - const [activeIndex, setActiveIndex] = useState(() => { - const initialTab = storedValue ?? defaultTab; - return tabs.findIndex((tab) => tab.key === initialTab); - }); - - useEffect(() => { - if (storeInLocalStorage && tabs[activeIndex]) { - setValue(tabs[activeIndex].key); - } - }, [activeIndex, setValue, storeInLocalStorage, tabs]); +function TabsTrigger({ + className, + size = "md", + ...props +}: React.ComponentProps & { size?: "sm" | "md" | "lg" }) { + return ( + + ); +} - const handleTabChange = (index: number) => { - setActiveIndex(index); - if (!tabs[index].disabled) { - tabs[index].onClick?.(); - } - }; +function TabsContent({ className, ...props }: React.ComponentProps) { + return ; +} +function TabsIndicator({ className, ...props }: React.ComponentProps<"div">) { return ( - -
- - {actions &&
{actions}
} -
- - {tabs.map((tab) => ( - - {tab.content} - - ))} -
+
); -}; +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, TabsIndicator }; diff --git a/packages/ui/src/tabs/composable-tabs.tsx b/packages/ui/src/tabs/composable-tabs.tsx new file mode 100644 index 00000000000..377d45af4be --- /dev/null +++ b/packages/ui/src/tabs/composable-tabs.tsx @@ -0,0 +1,59 @@ +import * as React from "react"; +import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs"; +import { cn } from "../utils"; + +// Root Tabs Container +function Tabs({ className, ...props }: React.ComponentProps) { + return ; +} + +// Tabs List Container +function TabsList({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +// Individual Tab Trigger +function TabsTrigger({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +// Tab Content Panel +function TabsContent({ className, ...props }: React.ComponentProps) { + return ; +} + +// Tab Indicator (optional, for visual feedback) +function TabsIndicator({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Tabs, TabsList, TabsTrigger, TabsContent, TabsIndicator }; From 0c645a77f9951686e6d3f164ea74dd1690945330 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:43:20 +0530 Subject: [PATCH 05/16] refactor: cleanup --- apps/web/core/components/core/image-picker-popover.tsx | 2 +- .../components/cycles/analytics-sidebar/issue-progress.tsx | 1 - .../components/cycles/analytics-sidebar/progress-stats.tsx | 2 -- .../components/license/modal/card/base-paid-plan-card.tsx | 1 - packages/ui/src/tabs/composable-tabs.tsx | 5 ----- 5 files changed, 1 insertion(+), 10 deletions(-) diff --git a/apps/web/core/components/core/image-picker-popover.tsx b/apps/web/core/components/core/image-picker-popover.tsx index 896c2c4cfb3..7f399c34ff8 100644 --- a/apps/web/core/components/core/image-picker-popover.tsx +++ b/apps/web/core/components/core/image-picker-popover.tsx @@ -362,7 +362,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { onClick={() => setImage(null)} disabled={isImageUploading} > - Remove + Cancel
)} diff --git a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx index a84475cad2d..1c2c10fa7d9 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/issue-progress.tsx @@ -178,7 +178,6 @@ export const CycleAnalyticsProgress: FC = observer((pro totalIssuesCount={estimateType === "points" ? totalEstimatePoints || 0 : totalIssues || 0} isEditable={Boolean(!peekCycle)} size="xs" - noBackground={false} filters={issueFilters} handleFiltersUpdate={handleFiltersUpdate} /> diff --git a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index 8b341b863c7..a7b6c6b4aba 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -247,8 +247,6 @@ export const CycleProgressStats: FC = observer((props) => { filters, handleFiltersUpdate, size = "sm", - - noBackground = false, } = props; // hooks const { storedValue: currentTab, setValue: setCycleTab } = useLocalStorage( diff --git a/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx b/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx index 16495e442d6..2f4b53356ec 100644 --- a/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx +++ b/apps/web/core/components/license/modal/card/base-paid-plan-card.tsx @@ -63,7 +63,6 @@ export const BasePaidPlanCard: FC = observer((props) =>
- {/* Features section - rendered outside tabs since it's common for all billing frequencies */}
{`Everything in ${basePlan} +`}
    diff --git a/packages/ui/src/tabs/composable-tabs.tsx b/packages/ui/src/tabs/composable-tabs.tsx index 377d45af4be..8337acf155a 100644 --- a/packages/ui/src/tabs/composable-tabs.tsx +++ b/packages/ui/src/tabs/composable-tabs.tsx @@ -2,12 +2,10 @@ import * as React from "react"; import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs"; import { cn } from "../utils"; -// Root Tabs Container function Tabs({ className, ...props }: React.ComponentProps) { return ; } -// Tabs List Container function TabsList({ className, ...props }: React.ComponentProps) { return ( ) { return ( ) { return ; } -// Tab Indicator (optional, for visual feedback) function TabsIndicator({ className, ...props }: React.ComponentProps<"div">) { return (
    Date: Wed, 20 Aug 2025 22:45:57 +0530 Subject: [PATCH 06/16] refactor: update image upload instructions for clarity --- apps/web/core/components/core/image-picker-popover.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/core/components/core/image-picker-popover.tsx b/apps/web/core/components/core/image-picker-popover.tsx index 7f399c34ff8..a551188b8e4 100644 --- a/apps/web/core/components/core/image-picker-popover.tsx +++ b/apps/web/core/components/core/image-picker-popover.tsx @@ -333,7 +333,7 @@ export const ImagePickerPopover: React.FC = observer((props) => {
    - {isDragActive ? "Drop the file here" : "Drag & drop or click to upload"} + {isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
From cf7a35ab739fd0a9f99c8df9ac9b8f5c467297f9 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:32:50 +0530 Subject: [PATCH 07/16] refactor: expose Tabs components for better modularity --- packages/propel/src/tabs/tabs.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/propel/src/tabs/tabs.tsx b/packages/propel/src/tabs/tabs.tsx index 19923491a07..11b972a6607 100644 --- a/packages/propel/src/tabs/tabs.tsx +++ b/packages/propel/src/tabs/tabs.tsx @@ -60,4 +60,9 @@ function TabsIndicator({ className, ...props }: React.ComponentProps<"div">) { ); } +Tabs.List = TabsList; +Tabs.Trigger = TabsTrigger; +Tabs.Content = TabsContent; +Tabs.Indicator = TabsIndicator; + export { Tabs, TabsList, TabsTrigger, TabsContent, TabsIndicator }; From 35f91c3d9b80229b6addc87ef610f9aa96327457 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:51:30 +0530 Subject: [PATCH 08/16] refactor: enhance type safety for tab change handlers across components --- .../[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx | 2 +- .../components/cycles/analytics-sidebar/progress-stats.tsx | 4 ++-- apps/web/core/components/pages/navigation-pane/root.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index 4bbc48cce96..388e3e96c0b 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -62,7 +62,7 @@ const AnalyticsPage = observer((props: Props) => { const ANALYTICS_TABS = useMemo(() => getAnalyticsTabs(t), [t]); const defaultTab = (tabId as TAnalyticsTabsBase) || ANALYTICS_TABS[0].key; - const handleTabChange = (value: string) => { + const handleTabChange = (value: TAnalyticsTabsBase) => { router.push(`/${currentWorkspace?.slug}/analytics/${value}`); }; diff --git a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index a7b6c6b4aba..ace85999344 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -299,8 +299,8 @@ export const CycleProgressStats: FC = observer((props) => { total: totalIssuesCount || 0, })); - const handleTabChange = (value: string) => { - setCycleTab(value as TCycleProgressStatsTabKey); + const handleTabChange = (value: TCycleProgressStatsTabKey) => { + setCycleTab(value); }; return ( diff --git a/apps/web/core/components/pages/navigation-pane/root.tsx b/apps/web/core/components/pages/navigation-pane/root.tsx index 71dd927c028..bb37ec32055 100644 --- a/apps/web/core/components/pages/navigation-pane/root.tsx +++ b/apps/web/core/components/pages/navigation-pane/root.tsx @@ -46,8 +46,8 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { const { t } = useTranslation(); const handleTabChange = useCallback( - (value: string) => { - const updatedTab = value as TPageNavigationPaneTab; + (value: TPageNavigationPaneTab) => { + const updatedTab = value; const isUpdatedTabInfo = updatedTab === "info"; const updatedRoute = updateQueryParams({ paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab }, From 8bf02b2a7fa636fcb97b9da14d3d5147f6f8a987 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:35:38 +0530 Subject: [PATCH 09/16] refactor: cleanup --- packages/propel/src/tabs/list.tsx | 58 ------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 packages/propel/src/tabs/list.tsx diff --git a/packages/propel/src/tabs/list.tsx b/packages/propel/src/tabs/list.tsx deleted file mode 100644 index 6155b10cebd..00000000000 --- a/packages/propel/src/tabs/list.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { FC } from "react"; -import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; -import { LucideProps } from "lucide-react"; -// helpers -import { cn } from "@plane/utils"; - -export type TabListItem = { - key: string; - icon?: FC; - label?: React.ReactNode; - disabled?: boolean; - onClick?: () => void; -}; - -type TTabListProps = { - tabs: TabListItem[]; - tabListClassName?: string; - tabClassName?: string; - size?: "sm" | "md" | "lg"; - selectedTab?: string; -}; - -export const TabList: FC = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab }) => ( - - {tabs.map((tab) => ( - - 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} - disabled={tab.disabled} - > - {tab.icon && } - {tab.label} - - ))} - - - -); From 8844a10339c16e4322a73affc8e2a9bfef0d08fd Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Tue, 16 Sep 2025 19:18:14 +0530 Subject: [PATCH 10/16] refactor: cleanup --- .../(projects)/analytics/[tabId]/page.tsx | 2 +- .../cycles/active-cycle/cycle-stats.tsx | 2 +- .../analytics-sidebar/progress-stats.tsx | 2 +- .../analytics-sidebar/progress-stats.tsx | 2 +- .../components/pages/navigation-pane/root.tsx | 10 ++-- .../pages/navigation-pane/tabs-list.tsx | 2 +- .../core/components/pages/version/editor.tsx | 2 +- packages/propel/package.json | 2 +- packages/ui/src/tabs/composable-tabs.tsx | 54 ------------------- packages/ui/src/tabs/tab-list.tsx | 9 +++- 10 files changed, 20 insertions(+), 67 deletions(-) delete mode 100644 packages/ui/src/tabs/composable-tabs.tsx diff --git a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx index 388e3e96c0b..5cbe9272d59 100644 --- a/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx +++ b/apps/web/app/(all)/[workspaceSlug]/(projects)/analytics/[tabId]/page.tsx @@ -6,8 +6,8 @@ import { useRouter } from "next/navigation"; // plane package imports import { EUserPermissions, EUserPermissionsLevel, PROJECT_TRACKER_ELEMENTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { TAnalyticsTabsBase } from "@plane/types"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; +import { TAnalyticsTabsBase } from "@plane/types"; // components import AnalyticsFilterActions from "@/components/analytics/analytics-filter-actions"; import { PageHead } from "@/components/core/page-title"; diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx index 218e8bfc087..8588f9fa8af 100644 --- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -7,8 +7,8 @@ import { CalendarCheck } from "lucide-react"; // plane imports import { useTranslation } from "@plane/i18n"; import { PriorityIcon } from "@plane/propel/icons"; -import { Tooltip } from "@plane/propel/tooltip"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; +import { Tooltip } from "@plane/propel/tooltip"; import { EIssuesStoreType, ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Loader, Avatar } from "@plane/ui"; diff --git a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx index a74ce9e90c2..a53f6a992f6 100644 --- a/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/cycles/analytics-sidebar/progress-stats.tsx @@ -4,8 +4,8 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTranslation } from "@plane/i18n"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { StateGroupIcon } from "@plane/propel/icons"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { IIssueFilterOptions, IIssueFilters, diff --git a/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx b/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx index 7cdf13e2ace..6538da2aded 100644 --- a/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx +++ b/apps/web/core/components/modules/analytics-sidebar/progress-stats.tsx @@ -4,8 +4,8 @@ import { FC } from "react"; import { observer } from "mobx-react"; import Image from "next/image"; import { useTranslation } from "@plane/i18n"; -import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { StateGroupIcon } from "@plane/propel/icons"; +import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { IIssueFilterOptions, IIssueFilters, diff --git a/apps/web/core/components/pages/navigation-pane/root.tsx b/apps/web/core/components/pages/navigation-pane/root.tsx index 7101c72982b..a4daa6832d4 100644 --- a/apps/web/core/components/pages/navigation-pane/root.tsx +++ b/apps/web/core/components/pages/navigation-pane/root.tsx @@ -2,9 +2,9 @@ import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useRouter, useSearchParams } from "next/navigation"; import { ArrowRightCircle } from "lucide-react"; +import { useTranslation } from "@plane/i18n"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; // plane imports -import { useTranslation } from "@plane/i18n"; import { Tooltip } from "@plane/propel/tooltip"; // hooks import { useQueryParams } from "@/hooks/use-query-params"; @@ -106,10 +106,10 @@ export const PageNavigationPaneRoot: React.FC = observer((props) => { ) : showNavigationTabs ? ( + value={activeTab} + onValueChange={handleTabChange} + className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none" + > diff --git a/apps/web/core/components/pages/navigation-pane/tabs-list.tsx b/apps/web/core/components/pages/navigation-pane/tabs-list.tsx index 20acd35c009..0f1dfba7350 100644 --- a/apps/web/core/components/pages/navigation-pane/tabs-list.tsx +++ b/apps/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -1,6 +1,6 @@ +import { useTranslation } from "@plane/i18n"; import { TabsList, TabsTrigger } from "@plane/propel/tabs"; // plane imports -import { useTranslation } from "@plane/i18n"; // plane web components import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane"; diff --git a/apps/web/core/components/pages/version/editor.tsx b/apps/web/core/components/pages/version/editor.tsx index d001072dbbe..c484a4ee5cb 100644 --- a/apps/web/core/components/pages/version/editor.tsx +++ b/apps/web/core/components/pages/version/editor.tsx @@ -3,8 +3,8 @@ import { useParams } from "next/navigation"; // plane imports import type { TDisplayConfig } from "@plane/editor"; import type { JSONContent, TPageVersion } from "@plane/types"; -import { isJSONContentEmpty } from "@plane/utils"; import { Loader } from "@plane/ui"; +import { isJSONContentEmpty } from "@plane/utils"; // components import { DocumentEditor } from "@/components/editor/document/editor"; // hooks diff --git a/packages/propel/package.json b/packages/propel/package.json index a4155131eee..6650905bb26 100644 --- a/packages/propel/package.json +++ b/packages/propel/package.json @@ -79,4 +79,4 @@ "tsdown": "catalog:", "typescript": "catalog:" } -} \ No newline at end of file +} diff --git a/packages/ui/src/tabs/composable-tabs.tsx b/packages/ui/src/tabs/composable-tabs.tsx deleted file mode 100644 index 8337acf155a..00000000000 --- a/packages/ui/src/tabs/composable-tabs.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from "react"; -import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs"; -import { cn } from "../utils"; - -function Tabs({ className, ...props }: React.ComponentProps) { - return ; -} - -function TabsList({ className, ...props }: React.ComponentProps) { - return ( - - ); -} - -function TabsTrigger({ className, ...props }: React.ComponentProps) { - return ( - - ); -} - -function TabsContent({ className, ...props }: React.ComponentProps) { - return ; -} - -function TabsIndicator({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ); -} - -export { Tabs, TabsList, TabsTrigger, TabsContent, TabsIndicator }; diff --git a/packages/ui/src/tabs/tab-list.tsx b/packages/ui/src/tabs/tab-list.tsx index a6a7a33bf94..77587d544fa 100644 --- a/packages/ui/src/tabs/tab-list.tsx +++ b/packages/ui/src/tabs/tab-list.tsx @@ -21,7 +21,14 @@ type TTabListProps = { onTabChange?: (key: TKey) => void; }; -export const TabList = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab, onTabChange }: TTabListProps) => ( +export const TabList = ({ + tabs, + tabListClassName, + tabClassName, + size = "md", + selectedTab, + onTabChange, +}: TTabListProps) => ( Date: Tue, 16 Sep 2025 19:26:56 +0530 Subject: [PATCH 11/16] refactor: cleanup --- .../cycles/active-cycle/cycle-stats.tsx | 4 ++-- packages/ui/src/tabs/tab-list.tsx | 17 +++++++++-------- packages/ui/src/tabs/tabs.tsx | 10 +++++----- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx index 8588f9fa8af..abdbc34b65b 100644 --- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -79,8 +79,8 @@ export const ActiveCycleStats: FC = observer((props) => { ); - const handleTabChange = (value: string) => { - setTab(value as TActiveCycleStatsTab); + const handleTabChange = (value: TActiveCycleStatsTab) => { + setTab(value); }; return cycleId ? ( diff --git a/packages/ui/src/tabs/tab-list.tsx b/packages/ui/src/tabs/tab-list.tsx index 77587d544fa..c60ab1fa775 100644 --- a/packages/ui/src/tabs/tab-list.tsx +++ b/packages/ui/src/tabs/tab-list.tsx @@ -4,32 +4,33 @@ import React, { FC } from "react"; // helpers import { cn } from "../utils"; -export type TabListItem = { - key: TKey; +export type TabListItem = { + key: string; icon?: FC; label?: React.ReactNode; disabled?: boolean; onClick?: () => void; }; -type TTabListProps = { - tabs: TabListItem[]; +type TTabListProps = { + tabs: TabListItem[]; tabListClassName?: string; tabClassName?: string; size?: "sm" | "md" | "lg"; - selectedTab?: TKey; - onTabChange?: (key: TKey) => void; + selectedTab?: string; + onTabChange?: (key: string) => void; }; -export const TabList = ({ +export const TabList: FC = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab, onTabChange, -}: TTabListProps) => ( +}) => ( = TabListItem & TabContent; +export type TabItem = TabListItem & TabContent; -type TTabsProps[]> = { - tabs: TTabs; +type TTabsProps = { + tabs: TabItem[]; storageKey?: string; actions?: React.ReactNode; - defaultTab?: TTabs[number]["key"]; + defaultTab?: string; containerClassName?: string; tabListContainerClassName?: string; tabListClassName?: string; @@ -26,7 +26,7 @@ type TTabsProps[]> = { storeInLocalStorage?: boolean; }; -export const Tabs = []>(props: TTabsProps) => { +export const Tabs: FC = (props: TTabsProps) => { const { tabs, storageKey, From 2036239d135a47966d8ca6d5f3128a4244372a64 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 17 Sep 2025 10:30:36 +0530 Subject: [PATCH 12/16] refactor: update image picker popover for improved tab handling and search functionality --- .../components/core/image-picker-popover.tsx | 134 +++++++++--------- 1 file changed, 65 insertions(+), 69 deletions(-) diff --git a/apps/web/core/components/core/image-picker-popover.tsx b/apps/web/core/components/core/image-picker-popover.tsx index a551188b8e4..c0d5d98d6e1 100644 --- a/apps/web/core/components/core/image-picker-popover.tsx +++ b/apps/web/core/components/core/image-picker-popover.tsx @@ -14,8 +14,6 @@ import { useOutsideClickDetector } from "@plane/hooks"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { EFileAssetType } from "@plane/types"; import { Button, Input, Loader, TOAST_TYPE, setToast } from "@plane/ui"; -// helpers -import { getFileURL } from "@plane/utils"; // hooks import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down"; // services @@ -180,9 +178,9 @@ export const ImagePickerPopover: React.FC = observer((props) => { >
- + {tabOptions.map((tab) => { if (!unsplashImages && unsplashError && tab.key === "unsplash") return null; @@ -196,72 +194,70 @@ export const ImagePickerPopover: React.FC = observer((props) => { })} - {(unsplashImages || !unsplashError) && ( - -
- ( - { - if (e.key === "Enter") { - e.preventDefault(); - setSearchParams(formData.search); - } + +
+ ( + { + if (e.key === "Enter") { + e.preventDefault(); + setSearchParams(formData.search); + } + }} + value={value} + onChange={(e) => setFormData({ ...formData, search: e.target.value })} + ref={ref} + placeholder="Search for images" + className="w-full text-sm" + /> + )} + /> + +
+ {unsplashImages ? ( + unsplashImages.length > 0 ? ( +
+ {unsplashImages.map((image) => ( +
{ + setIsOpen(false); + onChange(image.urls.regular); }} - value={value} - onChange={(e) => setFormData({ ...formData, search: e.target.value })} - ref={ref} - placeholder="Search for images" - className="w-full text-sm" - /> - )} - /> - -
- {unsplashImages ? ( - unsplashImages.length > 0 ? ( -
- {unsplashImages.map((image) => ( -
{ - setIsOpen(false); - onChange(image.urls.regular); - }} - > - {image.alt_description} -
- ))} -
- ) : ( -

No images found.

- ) + > + {image.alt_description} +
+ ))} +
) : ( - - - - - - - - - - - )} -
- )} +

No images found.

+ ) + ) : ( + + + + + + + + + + + )} + {(!projectCoverImages || projectCoverImages.length !== 0) && ( @@ -278,7 +274,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { }} > {`Project From 8da44bad068a4da618e0875b88ca5b42d85ab8f8 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 17 Sep 2025 14:17:31 +0530 Subject: [PATCH 13/16] refactor: enhance image picker popover search functionality and add aria-label to tabs list --- apps/web/core/components/core/image-picker-popover.tsx | 9 ++++++--- .../core/components/pages/navigation-pane/tabs-list.tsx | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/web/core/components/core/image-picker-popover.tsx b/apps/web/core/components/core/image-picker-popover.tsx index c0d5d98d6e1..0ce6478e84f 100644 --- a/apps/web/core/components/core/image-picker-popover.tsx +++ b/apps/web/core/components/core/image-picker-popover.tsx @@ -199,7 +199,7 @@ export const ImagePickerPopover: React.FC = observer((props) => { ( + render={({ field: { value, ref, onChange } }) => ( = observer((props) => { onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); - setSearchParams(formData.search); + setSearchParams(value); } }} value={value} - onChange={(e) => setFormData({ ...formData, search: e.target.value })} + onChange={(e) => { + onChange(e.target.value); + setFormData({ ...formData, search: e.target.value }); + }} ref={ref} placeholder="Search for images" className="w-full text-sm" diff --git a/apps/web/core/components/pages/navigation-pane/tabs-list.tsx b/apps/web/core/components/pages/navigation-pane/tabs-list.tsx index 0f1dfba7350..dbd549cfc0e 100644 --- a/apps/web/core/components/pages/navigation-pane/tabs-list.tsx +++ b/apps/web/core/components/pages/navigation-pane/tabs-list.tsx @@ -9,7 +9,7 @@ export const PageNavigationPaneTabsList = () => { const { t } = useTranslation(); return ( - + {ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => ( {t(tab.i18n_label)} From 0cfc605586c096b5d76346db73275c2d923c6204 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Wed, 17 Sep 2025 19:36:58 +0530 Subject: [PATCH 14/16] refactor: improve image picker popover layout and enhance file upload handling --- .../components/core/image-picker-popover.tsx | 127 +++++++----------- 1 file changed, 52 insertions(+), 75 deletions(-) diff --git a/apps/web/core/components/core/image-picker-popover.tsx b/apps/web/core/components/core/image-picker-popover.tsx index 0ce6478e84f..718c6d05ca7 100644 --- a/apps/web/core/components/core/image-picker-popover.tsx +++ b/apps/web/core/components/core/image-picker-popover.tsx @@ -7,13 +7,14 @@ import { useParams } from "next/navigation"; import { useDropzone } from "react-dropzone"; import { Control, Controller } from "react-hook-form"; import useSWR from "swr"; -import { Tab, Popover } from "@headlessui/react"; +import { Popover } from "@headlessui/react"; // plane imports import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; import { useOutsideClickDetector } from "@plane/hooks"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs"; import { EFileAssetType } from "@plane/types"; import { Button, Input, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +import { getFileURL } from "@plane/utils"; // hooks import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down"; // services @@ -302,98 +303,74 @@ export const ImagePickerPopover: React.FC = observer((props) => { )} - -
+ +
- -
-
-
- - - -
- + + {image !== null || (value && value !== "") ? ( + <> + image + + ) : ( +
+ {isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
- - {Object.keys(ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE).join(", ")} (Max{" "} - {MAX_FILE_SIZE / 1024 / 1024}MB) - -
+ )} + +
- {image && ( -
-
- Preview -
-
-

{image.name}

-

{(image.size / 1024 / 1024).toFixed(2)} MB

-
- -
- )} {fileRejections.length > 0 && ( -
-
- - - -
-
-

File rejected

-

{fileRejections[0].errors[0].message}

-
-
+

+ {fileRejections[0].errors[0].code === "file-too-large" + ? "The image size cannot exceed 5 MB." + : "Please upload a file in a valid format."} +

)} - {image && ( + +

File formats supported- .jpeg, .jpg, .png, .webp

+ +
+ - )} +
From 96ee0f739c95a5529d83413017ee725762786057 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 18 Sep 2025 01:43:43 +0530 Subject: [PATCH 15/16] refactor: enhance image picker popover layout and improve loading behavior --- .../components/core/image-picker-popover.tsx | 130 +++++++++--------- 1 file changed, 66 insertions(+), 64 deletions(-) diff --git a/apps/web/core/components/core/image-picker-popover.tsx b/apps/web/core/components/core/image-picker-popover.tsx index 718c6d05ca7..4d20a046429 100644 --- a/apps/web/core/components/core/image-picker-popover.tsx +++ b/apps/web/core/components/core/image-picker-popover.tsx @@ -195,72 +195,74 @@ export const ImagePickerPopover: React.FC = observer((props) => { })} - -
- ( - { - if (e.key === "Enter") { - e.preventDefault(); - setSearchParams(value); - } - }} - value={value} - onChange={(e) => { - onChange(e.target.value); - setFormData({ ...formData, search: e.target.value }); - }} - ref={ref} - placeholder="Search for images" - className="w-full text-sm" - /> - )} - /> - -
- {unsplashImages ? ( - unsplashImages.length > 0 ? ( -
- {unsplashImages.map((image) => ( -
{ - setIsOpen(false); - onChange(image.urls.regular); + +
+
+ ( + { + if (e.key === "Enter") { + e.preventDefault(); + setSearchParams(value); + } }} - > - {image.alt_description} -
- ))} -
+ value={value} + onChange={(e) => { + onChange(e.target.value); + setFormData({ ...formData, search: e.target.value }); + }} + ref={ref} + placeholder="Search for images" + className="w-full text-sm" + /> + )} + /> + +
+ {unsplashImages ? ( + unsplashImages.length > 0 ? ( +
+ {unsplashImages.map((image) => ( +
{ + setIsOpen(false); + onChange(image.urls.regular); + }} + > + {image.alt_description} +
+ ))} +
+ ) : ( +

No images found.

+ ) ) : ( -

No images found.

- ) - ) : ( - - - - - - - - - - - )} + + + + + + + + + + + )} +
{(!projectCoverImages || projectCoverImages.length !== 0) && ( From a8ece6ea1be9e508583e91f03c1a03da68ced9a6 Mon Sep 17 00:00:00 2001 From: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com> Date: Thu, 18 Sep 2025 19:50:37 +0530 Subject: [PATCH 16/16] refactor: added removed loader in active cycle --- .../web/core/components/cycles/active-cycle/cycle-stats.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx index abdbc34b65b..87b8f49b711 100644 --- a/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/apps/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -288,5 +288,9 @@ export const ActiveCycleStats: FC = observer((props) => {
- ) : null; + ) : ( + + + + ); });