Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca336aef9d | |||
| a8ece6ea1b | |||
| 96ee0f739c | |||
| 0cfc605586 | |||
| 8da44bad06 | |||
| 2036239d13 | |||
| cbd6e09408 | |||
| 8844a10339 | |||
| 52e91b1582 | |||
| 8bf02b2a7f | |||
| 35f91c3d9b | |||
| cf7a35ab73 | |||
| 5de96eee02 | |||
| 0c645a77f9 | |||
| 4f061399a5 | |||
| c0fb81271e | |||
| 3e571d4e4b | |||
| 813679d21f |
@@ -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
|
||||
|
||||
@@ -79,4 +79,4 @@
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user