Compare commits

...

18 Commits

Author SHA1 Message Date
Jayash Tripathy ca336aef9d Merge branch 'preview' of https://github.com/makeplane/plane into refactor-propel-tabs 2025-09-23 15:04:41 +05:30
Jayash Tripathy a8ece6ea1b refactor: added removed loader in active cycle 2025-09-18 19:50:37 +05:30
Jayash Tripathy 96ee0f739c refactor: enhance image picker popover layout and improve loading behavior 2025-09-18 01:43:43 +05:30
Jayash Tripathy 0cfc605586 refactor: improve image picker popover layout and enhance file upload handling 2025-09-17 19:36:58 +05:30
Jayash Tripathy 8da44bad06 refactor: enhance image picker popover search functionality and add aria-label to tabs list 2025-09-17 14:17:31 +05:30
Jayash Tripathy 2036239d13 refactor: update image picker popover for improved tab handling and search functionality 2025-09-17 10:30:36 +05:30
Jayash Tripathy cbd6e09408 refactor: cleanup 2025-09-16 19:26:56 +05:30
Jayash Tripathy 8844a10339 refactor: cleanup 2025-09-16 19:18:14 +05:30
Jayash Tripathy 52e91b1582 Merge remote-tracking branch 'origin/preview' into refactor-propel-tabs 2025-09-16 19:02:23 +05:30
Jayash Tripathy 8bf02b2a7f refactor: cleanup 2025-08-21 16:35:38 +05:30
Jayash Tripathy 35f91c3d9b refactor: enhance type safety for tab change handlers across components 2025-08-21 16:34:41 +05:30
Jayash Tripathy cf7a35ab73 refactor: expose Tabs components for better modularity 2025-08-21 16:34:36 +05:30
Jayash Tripathy 5de96eee02 refactor: update image upload instructions for clarity 2025-08-21 16:34:29 +05:30
Jayash Tripathy 0c645a77f9 refactor: cleanup 2025-08-21 16:34:01 +05:30
Jayash Tripathy 4f061399a5 refactor: migrate components to use propel/tabs for improved tab functionality and type safety 2025-08-21 16:33:59 +05:30
Jayash Tripathy c0fb81271e refacator: improved types of tabs and migrated some components to use propel/tabs 2025-08-21 16:33:27 +05:30
Jayash Tripathy 3e571d4e4b refactor: improve type definitions for TabListItem and TabItem 2025-08-21 16:32:14 +05:30
Jayash Tripathy 813679d21f refactor: type safety in tabs 2025-08-21 16:32:12 +05:30
15 changed files with 380 additions and 529 deletions
@@ -6,7 +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 { type TabItem, Tabs } from "@plane/ui";
import { Tabs } 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";
@@ -59,20 +60,11 @@ 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: <tab.content />,
onClick: () => {
router.push(`/${currentWorkspace?.slug}/analytics/${tab.key}`);
},
disabled: tab.isDisabled,
})),
[ANALYTICS_TABS, router, currentWorkspace?.slug]
);
const defaultTab = tabId || ANALYTICS_TABS[0].key;
const defaultTab = (tabId as TAnalyticsTabsBase) || ANALYTICS_TABS[0].key;
const handleTabChange = (value: TAnalyticsTabsBase) => {
router.push(`/${currentWorkspace?.slug}/analytics/${value}`);
};
return (
<>
@@ -80,19 +72,25 @@ const AnalyticsPage = observer((props: Props) => {
{workspaceProjectIds && (
<>
{workspaceProjectIds.length > 0 || loader === "init-loader" ? (
<div className="flex h-full overflow-hidden bg-custom-background-100 justify-between items-center ">
<Tabs
tabs={tabs}
storageKey={`analytics-page-${currentWorkspace?.id}`}
defaultTab={defaultTab}
size="md"
tabListContainerClassName="px-6 py-2 border-b border-custom-border-200 flex items-center justify-between"
tabListClassName="my-2 w-auto"
tabClassName="px-3"
tabPanelClassName="h-full overflow-hidden overflow-y-auto px-2"
storeInLocalStorage={false}
actions={<AnalyticsFilterActions />}
/>
<div className="flex h-full overflow-hidden bg-custom-background-100 justify-between items-center">
<Tabs value={defaultTab} onValueChange={handleTabChange} className="flex flex-col w-full h-full">
<div className="px-6 py-2 border-b border-custom-border-200 flex items-center justify-between">
<Tabs.List className="my-2 w-auto">
{ANALYTICS_TABS.map((tab) => (
<Tabs.Trigger key={tab.key} value={tab.key} size="md" className="px-3" disabled={tab.isDisabled}>
{tab.label}
</Tabs.Trigger>
))}
</Tabs.List>
<AnalyticsFilterActions />
</div>
{ANALYTICS_TABS.map((tab) => (
<Tabs.Content key={tab.key} value={tab.key} className="h-full overflow-hidden overflow-y-auto px-2">
<tab.content />
</Tabs.Content>
))}
</Tabs>
</div>
) : (
<DetailedEmptyState
@@ -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";
@@ -69,13 +68,11 @@ export const WorkItemsModalMainContent: React.FC<Props> = observer((props) => {
);
return (
<Tab.Group as={React.Fragment}>
<div className="flex flex-col gap-14 overflow-y-auto p-6">
<TotalInsights analyticsType="work-items" peekView={!fullScreen} />
<CreatedVsResolved />
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
<WorkItemsInsightTable />
</div>
</Tab.Group>
<div className="flex flex-col gap-14 overflow-y-auto p-6">
<TotalInsights analyticsType="work-items" peekView={!fullScreen} />
<CreatedVsResolved />
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
<WorkItemsInsightTable />
</div>
);
});
@@ -7,13 +7,13 @@ 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 } 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";
@@ -179,206 +179,203 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
>
<div
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 rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl md:h-[28rem] md:w-[36rem] overflow-hidden flex-1"
>
<Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
<Tabs defaultValue={tabOptions[0].key}>
<Tabs.List>
{tabOptions.map((tab) => {
if (!unsplashImages && unsplashError && tab.key === "unsplash") return null;
if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images") return null;
return (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded px-4 py-1 text-center text-sm outline-none transition-colors ${
selected ? "bg-custom-primary text-white" : "text-custom-text-100"
}`
}
>
<Tabs.Trigger key={tab.key} value={tab.key}>
{tab.title}
</Tab>
</Tabs.Trigger>
);
})}
</Tab.List>
<Tab.Panels className="vertical-scrollbar scrollbar-md h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
{(unsplashImages || !unsplashError) && (
<Tab.Panel className="mt-4 h-full w-full space-y-4">
<div className="flex gap-x-2">
<Controller
control={control}
name="search"
render={({ field: { value, ref } }) => (
<Input
id="search"
name="search"
type="text"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setSearchParams(formData.search);
}
</Tabs.List>
<Tabs.Content value="unsplash" className="pt-4 h-full overflow-hidden">
<div className="flex flex-col h-full">
<div className="flex gap-x-2">
<Controller
control={control}
name="search"
render={({ field: { value, ref, onChange } }) => (
<Input
id="search"
name="search"
type="text"
onKeyDown={(e) => {
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"
/>
)}
/>
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm">
Search
</Button>
</div>
{unsplashImages ? (
unsplashImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4 flex-1 overflow-y-auto pt-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 })}
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);
}}
>
<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">
<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>
)}
</Tab.Panel>
)}
{(!projectCoverImages || projectCoverImages.length !== 0) && (
<Tab.Panel className="mt-4 h-full w-full space-y-4">
{projectCoverImages ? (
projectCoverImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{projectCoverImages.map((image, index) => (
<div
key={image}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image);
}}
>
<img
src={image}
alt={`Default project cover image- ${index}`}
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 pt-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>
)}
</Tab.Panel>
)}
<Tab.Panel className="mt-4 h-full w-full">
<div className="flex h-full w-full flex-col gap-y-2">
<div className="flex w-full flex-1 items-center gap-3">
<div
{...getRootProps()}
className={`relative grid h-full w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
(image === null && isDragActive) || !value
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
: ""
}`}
>
<button
type="button"
className="absolute right-0 top-0 z-40 -translate-y-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
>
Edit
</button>
{image !== null || (value && value !== "") ? (
<>
<Image
layout="fill"
objectFit="cover"
src={image ? URL.createObjectURL(image) : value ? (getFileURL(value) ?? "") : ""}
alt="image"
className="rounded-lg"
>
<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>
<span className="mt-2 block text-sm font-medium text-custom-text-200">
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
</span>
</div>
)}
<input {...getInputProps()} />
))}
</div>
</div>
{fileRejections.length > 0 && (
<p className="text-sm text-red-500">
{fileRejections[0].errors[0].code === "file-too-large"
? "The image size cannot exceed 5 MB."
: "Please upload a file in a valid format."}
</p>
)}
) : (
<p className="pt-7 text-center text-xs text-custom-text-300">No images found.</p>
)
) : (
<Loader className="grid grid-cols-4 gap-4 flex-1 overflow-y-auto pt-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>
)}
</div>
</Tabs.Content>
<p className="text-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
{(!projectCoverImages || projectCoverImages.length !== 0) && (
<Tabs.Content value="images" className="pt-4 flex-1 h-full overflow-auto">
{projectCoverImages ? (
projectCoverImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{projectCoverImages.map((image, index) => (
<div
key={image}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image);
}}
>
<img
src={image}
alt={`Project cover ${index + 1}`}
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">
<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>
)}
</Tabs.Content>
)}
<div className="flex h-12 items-start justify-end gap-2">
<Button
variant="neutral-primary"
onClick={() => {
setIsOpen(false);
setImage(null);
}}
<Tabs.Content value="upload" className="pt-4 h-full">
<div className="flex h-full w-full flex-col gap-y-2">
<div className="flex w-full flex-1 items-center gap-3">
<div
{...getRootProps()}
className={`relative grid h-full w-full cursor-pointer place-items-center rounded-lg p-12 text-center focus:outline-none focus:ring-2 focus:ring-custom-primary focus:ring-offset-2 ${
(image === null && isDragActive) || !value
? "border-2 border-dashed border-custom-border-200 hover:bg-custom-background-90"
: ""
}`}
>
<button
type="button"
className="absolute right-0 top-0 z-40 -translate-y-1/2 rounded bg-custom-background-90 px-2 py-0.5 text-xs font-medium text-custom-text-200"
>
Cancel
</Button>
<Button
variant="primary"
className="w-full"
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
</Button>
Edit
</button>
{image !== null || (value && value !== "") ? (
<>
<Image
layout="fill"
objectFit="cover"
src={image ? URL.createObjectURL(image) : value ? (getFileURL(value) ?? "") : ""}
alt="image"
className="rounded-lg"
/>
</>
) : (
<div>
<span className="mt-2 block text-sm font-medium text-custom-text-200">
{isDragActive ? "Drop image here to upload" : "Drag & drop image here"}
</span>
</div>
)}
<input {...getInputProps()} />
</div>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
{fileRejections.length > 0 && (
<p className="text-sm text-red-500">
{fileRejections[0].errors[0].code === "file-too-large"
? "The image size cannot exceed 5 MB."
: "Please upload a file in a valid format."}
</p>
)}
<p className="text-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
<div className="flex h-12 items-start justify-end gap-2">
<Button
variant="neutral-primary"
onClick={() => {
setIsOpen(false);
setImage(null);
}}
>
Cancel
</Button>
<Button
variant="primary"
className="w-full"
onClick={handleSubmit}
disabled={!image}
loading={isImageUploading}
>
{isImageUploading ? "Uploading..." : "Upload & Save"}
</Button>
</div>
</div>
</Tabs.Content>
</Tabs>
</div>
</Popover.Panel>
)}
@@ -4,17 +4,16 @@ 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 { PriorityIcon } from "@plane/propel/icons";
import { Tabs } from "@plane/propel/tabs";
import { Tooltip } from "@plane/propel/tooltip";
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { EIssuesStoreType, ICycle } from "@plane/types";
// ui
import { Loader, 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";
@@ -40,10 +39,12 @@ export type ActiveCycleStatsProps = {
cycleIssueDetails: ActiveCycleIssueDetails;
};
export type TActiveCycleStatsTab = "Priority-Issues" | "Assignees" | "Labels";
export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props;
// local storage
const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees");
const { storedValue: tab, setValue: setTab } = useLocalStorage<TActiveCycleStatsTab>("activeCycleTab", "Assignees");
// refs
const issuesContainerRef = useRef<HTMLDivElement | null>(null);
// states
@@ -55,18 +56,6 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = 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);
@@ -74,6 +63,7 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
issue: { getIssueById },
setPeekIssue,
} = useIssueDetail();
const loadMoreIssues = useCallback(() => {
if (!cycleId) return;
fetchNextActiveCycleIssues(workspaceSlug, projectId, cycleId);
@@ -90,78 +80,27 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
</Loader>
);
const handleTabChange = (value: TActiveCycleStatsTab) => {
setTab(value);
};
return cycleId ? (
<div className="flex flex-col gap-4 p-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1 border border-custom-border-200 rounded-lg">
<Tab.Group
as={Fragment}
defaultIndex={currentValue(tab)}
onChange={(i) => {
switch (i) {
case 0:
return setTab("Priority-Issues");
case 1:
return setTab("Assignees");
case 2:
return setTab("Labels");
default:
return setTab("Priority-Issues");
}
}}
>
<Tab.List
as="div"
className="relative border-[0.5px] border-custom-border-200 rounded bg-custom-background-80 p-[1px] grid"
style={{
gridTemplateColumns: `repeat(3, 1fr)`,
}}
>
<Tab
className={({ selected }) =>
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,
}
)
}
>
<Tabs value={tab || "Assignees"} onValueChange={handleTabChange} className="flex flex-col w-full h-full">
<Tabs.List className="w-full">
<Tabs.Trigger value="Priority-Issues" size="sm">
{t("project_cycles.active_cycle.priority_issue")}
</Tab>
<Tab
className={({ selected }) =>
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,
}
)
}
>
</Tabs.Trigger>
<Tabs.Trigger value="Assignees" size="sm">
{t("project_cycles.active_cycle.assignees")}
</Tab>
<Tab
className={({ selected }) =>
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,
}
)
}
>
</Tabs.Trigger>
<Tabs.Trigger value="Labels" size="sm">
{t("project_cycles.active_cycle.labels")}
</Tab>
</Tab.List>
</Tabs.Trigger>
</Tabs.List>
<Tab.Panels as={Fragment}>
<Tab.Panel
as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
>
<Tabs.Content value="Priority-Issues" className="flex-1 mt-4">
<div className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm">
<div
ref={issuesContainerRef}
className="flex flex-col gap-1 h-full w-full overflow-y-auto vertical-scrollbar scrollbar-sm"
@@ -251,12 +190,11 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
loaders
)}
</div>
</Tab.Panel>
</div>
</Tabs.Content>
<Tab.Panel
as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
>
<Tabs.Content value="Assignees" className="flex-1 mt-4">
<div className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm">
{cycle && !isEmpty(cycle.distribution) ? (
cycle?.distribution?.assignees && cycle.distribution.assignees.length > 0 ? (
cycle.distribution?.assignees?.map((assignee, index) => {
@@ -313,12 +251,11 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
) : (
loaders
)}
</Tab.Panel>
</div>
</Tabs.Content>
<Tab.Panel
as="div"
className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm"
>
<Tabs.Content value="Labels" className="flex-1 mt-4">
<div className="flex h-52 w-full flex-col gap-1 overflow-y-auto text-custom-text-200 vertical-scrollbar scrollbar-sm">
{cycle && !isEmpty(cycle.distribution) ? (
cycle?.distribution?.labels && cycle.distribution.labels.length > 0 ? (
cycle.distribution.labels?.map((label, index) => (
@@ -356,9 +293,9 @@ export const ActiveCycleStats: FC<ActiveCycleStatsProps> = observer((props) => {
) : (
loaders
)}
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</Tabs.Content>
</Tabs>
</div>
) : (
<Loader className="flex flex-col gap-4 min-h-[17rem] overflow-hidden bg-custom-background-100 col-span-1 lg:col-span-2 xl:col-span-1">
@@ -145,9 +145,7 @@ export const CycleAnalyticsProgress: FC<TCycleAnalyticsProgress> = observer((pro
cycleId
)}
isEditable={Boolean(!peekCycle) && cycleFilter !== undefined}
noBackground={false}
plotType={plotType}
roundedTab={false}
selectedFilters={{
assignees: selectedAssignees,
labels: selectedLabels,
@@ -2,12 +2,12 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
import { toFilterArray } from "@plane/utils";
// components
import { AssigneeStatComponent, TAssigneeData } from "@/components/core/sidebar/progress-stats/assignee";
import { LabelStatComponent, TLabelData } from "@/components/core/sidebar/progress-stats/label";
@@ -27,9 +27,7 @@ type TCycleProgressStats = {
groupedIssues: Record<string, number>;
handleFiltersUpdate: (condition: TWorkItemFilterCondition) => void;
isEditable?: boolean;
noBackground?: boolean;
plotType: TCyclePlotType;
roundedTab?: boolean;
selectedFilters: TSelectedFilterProgressStats;
size?: "xs" | "sm";
totalIssuesCount: number;
@@ -42,9 +40,7 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
groupedIssues,
handleFiltersUpdate,
isEditable = false,
noBackground = false,
plotType,
roundedTab = false,
selectedFilters,
size = "sm",
totalIssuesCount,
@@ -57,7 +53,6 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
"stat-assignees"
);
// derived values
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TCycleDistribution;
const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
@@ -118,60 +113,43 @@ export const CycleProgressStats: FC<TCycleProgressStats> = observer((props) => {
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.List
as="div"
className={cn(
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
roundedTab ? `rounded-3xl` : `rounded-md`,
noBackground ? `` : `bg-custom-background-90`,
size === "xs" ? `text-xs` : `text-sm`
)}
>
<Tabs value={currentTab || "stat-assignees"} onValueChange={setCycleTab} className="flex flex-col w-full">
<Tabs.List>
{PROGRESS_STATS.map((stat) => (
<Tab
className={cn(
`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`,
stat.key === currentTab
? "bg-custom-background-100 text-custom-text-300"
: "text-custom-text-400 hover:text-custom-text-300"
)}
key={stat.key}
onClick={() => setCycleTab(stat.key)}
>
<Tabs.Trigger key={stat.key} value={stat.key} size={size === "xs" ? "sm" : "md"}>
{t(stat.i18n_title)}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
<Tab.Panels className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-states"}>
<StateGroupStatComponent
distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
isEditable={isEditable}
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tab.Panel>
<Tab.Panel key={"stat-assignees"}>
<AssigneeStatComponent
distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-labels"}>
<LabelStatComponent
distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable}
selectedLabelIds={selectedLabelIds}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tabs.List>
<Tabs.Content value="stat-states" className="py-3 text-custom-text-200">
<StateGroupStatComponent
distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
isEditable={isEditable}
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tabs.Content>
<Tabs.Content value="stat-assignees" className="py-3 text-custom-text-200">
<AssigneeStatComponent
distribution={distributionAssigneeData}
isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
/>
</Tabs.Content>
<Tabs.Content value="stat-labels" className="py-3 text-custom-text-200">
<LabelStatComponent
distribution={distributionLabelData}
isEditable={isEditable}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
selectedLabelIds={selectedLabelIds}
/>
</Tabs.Content>
</Tabs>
</div>
);
});
@@ -3,8 +3,8 @@
import { FC, useState } from "react";
import { observer } from "mobx-react";
import { CheckCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports
import { Tabs } from "@plane/propel/tabs";
// helpers
import { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types";
import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui";
@@ -40,59 +40,46 @@ export const BasePaidPlanCard: FC<TBasePaidPlanCardProps> = observer((props) =>
return (
<div className={cn("flex flex-col py-6 px-3", upgradeCardVariantStyle)}>
<Tab.Group selectedIndex={selectedPlan === "month" ? 0 : 1}>
<div className="flex w-full justify-center h-9">
<Tab.List
className={cn("flex space-x-1 rounded-md p-0.5 w-60", getSubscriptionBackgroundColor(planVariant, "50"))}
>
<div className="flex w-full justify-center">
<Tabs value={selectedPlan} onValueChange={setSelectedPlan} className="w-full">
<Tabs.List className={cn("w-60 mx-auto mb-2", getSubscriptionBackgroundColor(planVariant, "50"))}>
{prices.map((price: TSubscriptionPrice) => (
<Tab
key={price.key}
className={({ selected }) =>
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)}
>
<Tabs.Trigger key={price.recurring} value={price.recurring} className="text-sm">
{renderPriceContent(price)}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
</div>
<Tab.Panels>
</Tabs.List>
{prices.map((price: TSubscriptionPrice) => (
<Tab.Panel key={price.key}>
<div className="pt-6 text-center">
<Tabs.Content key={price.recurring} value={price.recurring} className="px-2 pb-2">
<div className="text-center">
<div className="text-xl font-medium">Plane {planeName}</div>
{renderActionButton(price)}
</div>
<div className="px-2 pt-6 pb-2">
<div className="p-2 text-sm font-semibold">{`Everything in ${basePlan} +`}</div>
<ul className="grid grid-cols-12 gap-x-4">
{features.map((feature) => (
<li
key={feature}
className={cn("col-span-12 relative rounded-md p-2 flex", {
"sm:col-span-6": !verticalFeatureList,
})}
>
<p className="w-full text-sm font-medium leading-5 flex items-center line-clamp-1">
<CheckCircle className="h-4 w-4 mr-2 text-custom-text-300 flex-shrink-0" />
<span className="text-custom-text-200 truncate">{feature}</span>
</p>
</li>
))}
</ul>
{extraFeatures && <div>{extraFeatures}</div>}
</div>
</Tab.Panel>
</Tabs.Content>
))}
</Tab.Panels>
</Tab.Group>
</Tabs>
</div>
<div className="px-2 pt-6 pb-2">
<div className="p-2 text-sm font-semibold">{`Everything in ${basePlan} +`}</div>
<ul className="grid grid-cols-12 gap-x-4">
{features.map((feature) => (
<li
key={feature}
className={cn("col-span-12 relative rounded-md p-2 flex", {
"sm:col-span-6": !verticalFeatureList,
})}
>
<p className="w-full text-sm font-medium leading-5 flex items-center line-clamp-1">
<CheckCircle className="h-4 w-4 mr-2 text-custom-text-300 flex-shrink-0" />
<span className="text-custom-text-200 truncate">{feature}</span>
</p>
</li>
))}
</ul>
{extraFeatures && <div>{extraFeatures}</div>}
</div>
</div>
);
});
@@ -207,9 +207,7 @@ export const ModuleAnalyticsProgress: FC<TModuleAnalyticsProgress> = observer((p
)}
isEditable={Boolean(!peekModule) && moduleFilter !== undefined}
moduleId={moduleId}
noBackground={false}
plotType={plotType}
roundedTab={false}
selectedFilters={{
assignees: selectedAssignees,
labels: selectedLabels,
@@ -2,8 +2,8 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import { TWorkItemFilterCondition } from "@plane/shared-state";
import { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
@@ -40,9 +40,7 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
handleFiltersUpdate,
isEditable = false,
moduleId,
noBackground = false,
plotType,
roundedTab = false,
selectedFilters,
size = "sm",
totalIssuesCount,
@@ -55,7 +53,6 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
"stat-assignees"
);
// derived values
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TModuleDistribution;
const currentEstimateDistribution = distribution as TModuleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
@@ -116,60 +113,42 @@ export const ModuleProgressStats: FC<TModuleProgressStats> = observer((props) =>
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.List
as="div"
className={cn(
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
roundedTab ? `rounded-3xl` : `rounded-md`,
noBackground ? `` : `bg-custom-background-90`,
size === "xs" ? `text-xs` : `text-sm`
)}
>
<Tabs value={currentTab || "stat-assignees"} onValueChange={setModuleTab} className="flex flex-col w-full">
<Tabs className={cn("flex w-full items-center justify-between gap-2 rounded-md p-1")}>
{PROGRESS_STATS.map((stat) => (
<Tab
className={cn(
`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`,
stat.key === currentTab
? "bg-custom-background-100 text-custom-text-300"
: "text-custom-text-400 hover:text-custom-text-300"
)}
key={stat.key}
onClick={() => setModuleTab(stat.key)}
>
<Tabs.Trigger key={stat.key} value={stat.key} size={size === "xs" ? "sm" : "md"}>
{t(stat.i18n_title)}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
<Tab.Panels className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-assignees"}>
<AssigneeStatComponent
distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-labels"}>
<LabelStatComponent
distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable}
selectedLabelIds={selectedLabelIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-states"}>
<StateGroupStatComponent
distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
isEditable={isEditable}
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tabs>
<Tabs.Content value="stat-assignees" className="py-3 text-custom-text-200">
<AssigneeStatComponent
distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds}
/>
</Tabs.Content>
<Tabs.Content value="stat-labels" className="py-3 text-custom-text-200">
<LabelStatComponent
distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable}
selectedLabelIds={selectedLabelIds}
/>
</Tabs.Content>
<Tabs.Content value="stat-states" className="py-3 text-custom-text-200">
<StateGroupStatComponent
distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
isEditable={isEditable}
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tabs.Content>
</Tabs>
</div>
);
});
@@ -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 { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@plane/propel/tabs";
// plane imports
import { Tooltip } from "@plane/propel/tooltip";
// hooks
import { useQueryParams } from "@/hooks/use-query-params";
@@ -49,7 +49,6 @@ export const PageNavigationPaneRoot: React.FC<Props> = 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);
// Check if any extension is currently active based on query parameters
const ActiveExtension = extensions.find((extension) => {
@@ -69,8 +68,8 @@ export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
const { t } = useTranslation();
const handleTabChange = useCallback(
(index: number) => {
const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index];
(value: TPageNavigationPaneTab) => {
const updatedTab = value;
const isUpdatedTabInfo = updatedTab === "info";
const updatedRoute = updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
@@ -106,10 +105,14 @@ export const PageNavigationPaneRoot: React.FC<Props> = observer((props) => {
{ActiveExtension ? (
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
) : showNavigationTabs ? (
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none"
>
<PageNavigationPaneTabsList />
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
</Tab.Group>
</Tabs>
) : null}
</div>
</aside>
@@ -107,13 +107,13 @@ export const PageNavigationPaneAssetsTabPanel: React.FC<Props> = observer((props
editor: { assetsList },
} = page;
if (assetsList.length === 0) return <PageNavigationPaneAssetsTabEmptyState />;
return (
<div className="mt-5 space-y-4">
{assetsList?.map((asset) => (
<AssetItem key={asset.id} asset={asset} page={page} />
))}
<div className="size-full pt-3 space-y-1">
{assetsList?.length === 0 ? (
<PageNavigationPaneAssetsTabEmptyState />
) : (
assetsList?.map((asset) => <AssetItem key={asset.id} asset={asset} page={page} />)
)}
</div>
);
});
@@ -1,5 +1,5 @@
import React from "react";
import { Tab } from "@headlessui/react";
import { Tabs } 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> = (props) => {
const { page, versionHistory } = props;
return (
<Tab.Panels as={React.Fragment}>
<>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab.Panel
key={tab.key}
as="div"
className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none"
>
<Tabs.Content key={tab.key} value={tab.key}>
{tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
<PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} page={page} />
</Tab.Panel>
</Tabs.Content>
))}
</Tab.Panels>
</>
);
};
@@ -1,6 +1,6 @@
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
// plane imports
// plane web components
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
@@ -9,29 +9,12 @@ export const PageNavigationPaneTabsList = () => {
const { t } = useTranslation();
return (
<Tab.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80 mx-3.5">
{({ selectedIndex }) => (
<>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
type="button"
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
>
{t(tab.i18n_label)}
</Tab>
))}
{/* active tab indicator */}
<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>
<Tabs.List aria-label="Page navigation tabs">
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tabs.Trigger key={tab.key} value={tab.key} type="button">
{t(tab.i18n_label)}
</Tabs.Trigger>
))}
</Tabs.List>
);
};
@@ -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
+1 -1
View File
@@ -79,4 +79,4 @@
"tsdown": "catalog:",
"typescript": "catalog:"
}
}
}