fix: merge conflicts resolved from develop

This commit is contained in:
Aaryan Khandelwal 2024-02-09 16:34:11 +05:30
commit 14664073ac
34 changed files with 983 additions and 359 deletions

View File

@ -20,6 +20,7 @@ from django.core import serializers
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework.response import Response
@ -312,6 +313,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
"labels": label_distribution,
"completion_chart": {},
}
if data[0]["start_date"] and data[0]["end_date"]:
data[0]["distribution"][
"completion_chart"
@ -840,10 +842,230 @@ class TransferCycleIssueEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
new_cycle = Cycle.objects.get(
new_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=new_cycle_id
).first()
old_cycle = (
Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=cycle_id
)
.annotate(
total_issues=Count(
"issue_cycle",
filter=Q(
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
completed_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
cancelled_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="cancelled",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
unstarted_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="unstarted",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
backlog_issues=Count(
"issue_cycle__issue__state__group",
filter=Q(
issue_cycle__issue__state__group="backlog",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
total_estimates=Sum("issue_cycle__issue__estimate_point")
)
.annotate(
completed_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="completed",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
.annotate(
started_estimates=Sum(
"issue_cycle__issue__estimate_point",
filter=Q(
issue_cycle__issue__state__group="started",
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
),
)
)
)
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle.first(),
slug=slug,
project_id=project_id,
cycle_id=cycle_id,
)
assignee_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.annotate(
total_issues=Count(
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
),
)
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("display_name")
)
label_distribution = (
Issue.objects.filter(
issue_cycle__cycle_id=cycle_id,
workspace__slug=slug,
project_id=project_id,
)
.annotate(label_name=F("labels__name"))
.annotate(color=F("labels__color"))
.annotate(label_id=F("labels__id"))
.values("label_name", "color", "label_id")
.annotate(
total_issues=Count(
"id",
filter=Q(archived_at__isnull=True, is_draft=False),
)
)
.annotate(
completed_issues=Count(
"id",
filter=Q(
completed_at__isnull=False,
archived_at__isnull=True,
is_draft=False,
),
)
)
.annotate(
pending_issues=Count(
"id",
filter=Q(
completed_at__isnull=True,
archived_at__isnull=True,
is_draft=False,
),
)
)
.order_by("label_name")
)
assignee_distribution_data = [
{
"display_name": item["display_name"],
"assignee_id": str(item["assignee_id"]) if item["assignee_id"] else None,
"avatar": item["avatar"],
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in assignee_distribution
]
label_distribution_data = [
{
"label_name": item["label_name"],
"color": item["color"],
"label_id": str(item["label_id"]) if item["label_id"] else None,
"total_issues": item["total_issues"],
"completed_issues": item["completed_issues"],
"pending_issues": item["pending_issues"],
}
for item in label_distribution
]
current_cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, pk=cycle_id
).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.first().total_issues,
"completed_issues": old_cycle.first().completed_issues,
"cancelled_issues": old_cycle.first().cancelled_issues,
"started_issues": old_cycle.first().started_issues,
"unstarted_issues": old_cycle.first().unstarted_issues,
"backlog_issues": old_cycle.first().backlog_issues,
"total_estimates": old_cycle.first().total_estimates,
"completed_estimates": old_cycle.first().completed_estimates,
"started_estimates": old_cycle.first().started_estimates,
"distribution":{
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
"completion_chart": completion_chart,
},
}
current_cycle.save(update_fields=["progress_snapshot"])
if (
new_cycle.end_date is not None
and new_cycle.end_date < timezone.now().date()

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.7 on 2024-02-08 09:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0059_auto_20240208_0957'),
]
operations = [
migrations.AddField(
model_name='cycle',
name='progress_snapshot',
field=models.JSONField(default=dict),
),
]

View File

@ -68,6 +68,7 @@ class Cycle(ProjectBaseModel):
sort_order = models.FloatField(default=65535)
external_source = models.CharField(max_length=255, null=True, blank=True)
external_id = models.CharField(max_length=255, blank=True, null=True)
progress_snapshot = models.JSONField(default=dict)
class Meta:
verbose_name = "Cycle"

View File

@ -31,6 +31,7 @@ export interface ICycle {
issue: string;
name: string;
owned_by: string;
progress_snapshot: TProgressSnapshot;
project: string;
project_detail: IProjectLite;
status: TCycleGroups;
@ -49,6 +50,23 @@ export interface ICycle {
workspace_detail: IWorkspaceLite;
}
export type TProgressSnapshot = {
backlog_issues: number;
cancelled_issues: number;
completed_estimates: number | null;
completed_issues: number;
distribution?: {
assignees: TAssigneesDistribution[];
completion_chart: TCompletionChartDistribution;
labels: TLabelsDistribution[];
};
started_estimates: number | null;
started_issues: number;
total_estimates: number | null;
total_issues: number;
unstarted_issues: number;
};
export type TAssigneesDistribution = {
assignee_id: string | null;
avatar: string | null;

View File

@ -221,3 +221,12 @@ export interface IGroupByColumn {
export interface IIssueMap {
[key: string]: TIssue;
}
export interface IIssueListRow {
id: string;
groupId: string;
type: "HEADER" | "NO_ISSUES" | "QUICK_ADD" | "ISSUE";
name?: string;
icon?: ReactElement | undefined;
payload?: Partial<TIssue>;
}

View File

@ -54,10 +54,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
setIsOpen(false);
};
const handleOnChange = () => {
if (closeOnSelect) closeDropdown();
};
const selectActiveItem = () => {
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
`[data-headlessui-state="active"] button`

View File

@ -10,6 +10,8 @@ import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSi
import { IAnalyticsParams } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper";
import { useApplication } from "hooks/store";
type Props = {
additionalParams?: Partial<IAnalyticsParams>;
@ -46,11 +48,13 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
);
const { theme: themeStore } = useApplication();
const isProjectLevel = projectId ? true : false;
return (
<div className={`flex flex-col-reverse overflow-hidden ${fullScreen ? "md:grid md:h-full md:grid-cols-4" : ""}`}>
<div className="col-span-3 flex h-full flex-col overflow-hidden">
<div className={cn("relative w-full h-full flex overflow-hidden", isProjectLevel ? "flex-col-reverse" : "")}>
<div className="w-full flex h-full flex-col overflow-hidden">
<CustomAnalyticsSelectBar
control={control}
setValue={setValue}
@ -61,16 +65,22 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
<CustomAnalyticsMainContent
analytics={analytics}
error={analyticsError}
fullScreen={fullScreen}
params={params}
fullScreen={fullScreen}
/>
</div>
<CustomAnalyticsSidebar
analytics={analytics}
params={params}
fullScreen={fullScreen}
isProjectLevel={isProjectLevel}
/>
<div
className={cn(
"border-l border-custom-border-200 transition-all",
!isProjectLevel
? "absolute right-0 top-0 bottom-0 md:relative flex-shrink-0 h-full max-w-[250px] sm:max-w-full"
: ""
)}
style={themeStore.workspaceAnalyticsSidebarCollapsed ? { right: `-${window?.innerWidth || 0}px` } : {}}
>
<CustomAnalyticsSidebar analytics={analytics} params={params} isProjectLevel={isProjectLevel} />
</div>
</div>
);
});

View File

@ -22,9 +22,8 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
return (
<div
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-3" : "grid-cols-2"} ${
fullScreen ? "md:py-5 lg:grid-cols-4" : ""
}`}
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-2"} ${fullScreen ? "md:py-5 lg:grid-cols-4" : ""
}`}
>
{!isProjectLevel && (
<div>

View File

@ -17,9 +17,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
const { getProjectById } = useProject();
return (
<div className="hidden h-full overflow-hidden md:flex md:flex-col">
<div className="relative flex flex-col gap-4 h-full">
<h4 className="font-medium">Selected Projects</h4>
<div className="mt-4 h-full space-y-6 overflow-y-auto">
<div className="relative space-y-6 overflow-hidden overflow-y-auto">
{projectIds.map((projectId) => {
const project = getProjectById(projectId);

View File

@ -26,7 +26,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
<>
{projectId ? (
cycleDetails ? (
<div className="hidden h-full overflow-y-auto md:block">
<div className="h-full overflow-y-auto">
<h4 className="break-words font-medium">Analytics for {cycleDetails.name}</h4>
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2 text-xs">
@ -52,7 +52,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
</div>
</div>
) : moduleDetails ? (
<div className="hidden h-full overflow-y-auto md:block">
<div className="h-full overflow-y-auto">
<h4 className="break-words font-medium">Analytics for {moduleDetails.name}</h4>
<div className="mt-4 space-y-4">
<div className="flex items-center gap-2 text-xs">
@ -78,7 +78,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
</div>
</div>
) : (
<div className="hidden h-full overflow-y-auto md:flex md:flex-col">
<div className="h-full overflow-y-auto">
<div className="flex items-center gap-1">
{projectDetails?.emoji ? (
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>

View File

@ -19,18 +19,18 @@ import { renderFormattedDate } from "helpers/date-time.helper";
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
// fetch-keys
import { ANALYTICS } from "constants/fetch-keys";
import { cn } from "helpers/common.helper";
type Props = {
analytics: IAnalyticsResponse | undefined;
params: IAnalyticsParams;
fullScreen: boolean;
isProjectLevel: boolean;
};
const analyticsService = new AnalyticsService();
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
const { analytics, params, fullScreen, isProjectLevel = false } = props;
const { analytics, params, isProjectLevel = false } = props;
// router
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@ -140,16 +140,16 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
return (
<div
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
fullScreen
? "overflow-hidden border-l border-neutral-border-medium md:h-full md:flex-col md:items-start md:space-y-4 md:border-l md:border-neutral-border-medium md:py-5"
: ""
}`}
className={cn(
"relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-sidebar-neutral-component-surface-light",
!isProjectLevel ? "flex-col" : ""
)}
>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-1 rounded-md bg-neutral-component-surface-dark px-3 py-1 text-xs text-neutral-text-medium">
<LayersIcon height={14} width={14} />
{analytics ? analytics.total : "..."} Issues
{analytics ? analytics.total : "..."}{" "}
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
</div>
{isProjectLevel && (
<div className="flex items-center gap-1 rounded-md bg-neutral-component-surface-dark px-3 py-1 text-xs text-neutral-text-medium">
@ -164,30 +164,30 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
</div>
)}
</div>
<div className="h-full w-full overflow-hidden">
{fullScreen ? (
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
)}
<CustomAnalyticsSidebarHeader />
</>
) : null}
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
<>
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
)}
<CustomAnalyticsSidebarHeader />
</>
</div>
<div className="flex flex-wrap items-center gap-2 justify-self-end">
<div className="flex flex-wrap items-center gap-2 justify-end">
<Button
variant="outline-neutral"
prependIcon={<RefreshCw className="h-3.5 w-3.5" />}
prependIcon={<RefreshCw className="h-3 md:h-3.5 w-3 md:w-3.5" />}
onClick={() => {
if (!workspaceSlug) return;
mutate(ANALYTICS(workspaceSlug.toString(), params));
}}
>
Refresh
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Refresh</div>
</Button>
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
Export as CSV
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Export as CSV</div>
</Button>
</div>
</div>

View File

@ -20,13 +20,15 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
return (
<Tab.Group as={React.Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-neutral-border-medium p-5 pt-0">
<Tab.List as="div" className="flex space-x-2 border-b border-neutral-border-medium px-0 md:px-5 py-0 md:py-3">
{ANALYTICS_TABS.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-3xl border border-neutral-border-medium px-4 py-2 text-xs hover:bg-neutral-component-surface-dark ${
selected ? "bg-neutral-component-surface-dark" : ""
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-neutral-border-medium focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-neutral-component-surface-dark ${
selected
? "border-primary-border-subtle text-primary-text-subtle md:bg-neutral-component-surface-dark md:text-neutral-text-medium md:border-neutral-border-medium"
: "border-transparent"
}`
}
onClick={() => {}}

View File

@ -0,0 +1,80 @@
import { cn } from "helpers/common.helper";
import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react";
type Props = {
defaultHeight?: string;
verticalOffset?: number;
horizonatlOffset?: number;
root?: MutableRefObject<HTMLElement | null>;
children: ReactNode;
as?: keyof JSX.IntrinsicElements;
classNames?: string;
alwaysRender?: boolean;
placeholderChildren?: ReactNode;
pauseHeightUpdateWhileRendering?: boolean;
changingReference?: any;
};
const RenderIfVisible: React.FC<Props> = (props) => {
const {
defaultHeight = "300px",
root,
verticalOffset = 50,
horizonatlOffset = 0,
as = "div",
children,
classNames = "",
alwaysRender = false, //render the children even if it is not visble in root
placeholderChildren = null, //placeholder children
pauseHeightUpdateWhileRendering = false, //while this is true the height of the blocks are maintained
changingReference, //This is to force render when this reference is changed
} = props;
const [shouldVisible, setShouldVisible] = useState<boolean>(alwaysRender);
const placeholderHeight = useRef<string>(defaultHeight);
const intersectionRef = useRef<HTMLElement | null>(null);
const isVisible = alwaysRender || shouldVisible;
// Set visibility with intersection observer
useEffect(() => {
if (intersectionRef.current) {
const observer = new IntersectionObserver(
(entries) => {
if (typeof window !== undefined && window.requestIdleCallback) {
window.requestIdleCallback(() => setShouldVisible(entries[0].isIntersecting), {
timeout: 300,
});
} else {
setShouldVisible(entries[0].isIntersecting);
}
},
{
root: root?.current,
rootMargin: `${verticalOffset}% ${horizonatlOffset}% ${verticalOffset}% ${horizonatlOffset}%`,
}
);
observer.observe(intersectionRef.current);
return () => {
if (intersectionRef.current) {
observer.unobserve(intersectionRef.current);
}
};
}
}, [root?.current, intersectionRef, children, changingReference]);
//Set height after render
useEffect(() => {
if (intersectionRef.current && isVisible) {
placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`;
}
}, [isVisible, intersectionRef, alwaysRender, pauseHeightUpdateWhileRendering]);
const child = isVisible ? <>{children}</> : placeholderChildren;
const style =
isVisible && !pauseHeightUpdateWhileRendering ? {} : { height: placeholderHeight.current, width: "100%" };
const className = isVisible ? classNames : cn(classNames, "bg-custom-background-80");
return React.createElement(as, { ref: intersectionRef, style, className }, child);
};
export default RenderIfVisible;

View File

@ -3,6 +3,7 @@ import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
import { useForm } from "react-hook-form";
import { Disclosure, Popover, Transition } from "@headlessui/react";
import isEmpty from "lodash/isEmpty";
// services
import { CycleService } from "services/cycle.service";
// hooks
@ -293,7 +294,11 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
const progressPercentage = cycleDetails
? Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
? isCompleted
? Math.round(
(cycleDetails.progress_snapshot.completed_issues / cycleDetails.progress_snapshot.total_issues) * 100
)
: Math.round((cycleDetails.completed_issues / cycleDetails.total_issues) * 100)
: null;
if (!cycleDetails)
@ -317,7 +322,14 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
const issueCount =
cycleDetails.total_issues === 0 ? "0 Issue" : `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
isCompleted && !isEmpty(cycleDetails.progress_snapshot)
? cycleDetails.progress_snapshot.total_issues === 0
? "0 Issue"
: `${cycleDetails.progress_snapshot.completed_issues}/${cycleDetails.progress_snapshot.total_issues}`
: cycleDetails.total_issues === 0
? "0 Issue"
: `${cycleDetails.completed_issues}/${cycleDetails.total_issues}`;
const daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
@ -568,49 +580,105 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
<Transition show={open}>
<Disclosure.Panel>
<div className="flex flex-col gap-3">
{cycleDetails.distribution?.completion_chart &&
cycleDetails.start_date &&
cycleDetails.end_date ? (
<div className="h-full w-full pt-4">
<div className="flex items-start gap-4 py-2 text-xs">
<div className="flex items-center gap-3 text-neutral-text-strong">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
<>
{cycleDetails.progress_snapshot.distribution?.completion_chart &&
cycleDetails.start_date &&
cycleDetails.end_date && (
<div className="h-full w-full pt-4">
<div className="flex items-start gap-4 py-2 text-xs">
<div className="flex items-center gap-3 text-neutral-text-strong">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.progress_snapshot.distribution?.completion_chart}
startDate={cycleDetails.start_date}
endDate={cycleDetails.end_date}
totalIssues={cycleDetails.progress_snapshot.total_issues}
/>
</div>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.distribution?.completion_chart}
startDate={cycleDetails.start_date}
endDate={cycleDetails.end_date}
totalIssues={cycleDetails.total_issues}
/>
</div>
</div>
)}
</>
) : (
""
<>
{cycleDetails.distribution?.completion_chart &&
cycleDetails.start_date &&
cycleDetails.end_date && (
<div className="h-full w-full pt-4">
<div className="flex items-start gap-4 py-2 text-xs">
<div className="flex items-center gap-3 text-neutral-text-strong">
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
<span>Ideal</span>
</div>
<div className="flex items-center justify-center gap-1">
<span className="h-2.5 w-2.5 rounded-full bg-[#4C8FFF]" />
<span>Current</span>
</div>
</div>
</div>
<div className="relative h-40 w-80">
<ProgressChart
distribution={cycleDetails.distribution?.completion_chart}
startDate={cycleDetails.start_date}
endDate={cycleDetails.end_date}
totalIssues={cycleDetails.total_issues}
/>
</div>
</div>
)}
</>
)}
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
<div className="h-full w-full border-t border-neutral-border-medium pt-5">
<SidebarProgressStats
distribution={cycleDetails.distribution}
groupedIssues={{
backlog: cycleDetails.backlog_issues,
unstarted: cycleDetails.unstarted_issues,
started: cycleDetails.started_issues,
completed: cycleDetails.completed_issues,
cancelled: cycleDetails.cancelled_issues,
}}
totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)}
/>
</div>
{/* stats */}
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
<>
{cycleDetails.progress_snapshot.total_issues > 0 &&
cycleDetails.progress_snapshot.distribution && (
<div className="h-full w-full border-t border-neutral-border-medium pt-5">
<SidebarProgressStats
distribution={cycleDetails.progress_snapshot.distribution}
groupedIssues={{
backlog: cycleDetails.progress_snapshot.backlog_issues,
unstarted: cycleDetails.progress_snapshot.unstarted_issues,
started: cycleDetails.progress_snapshot.started_issues,
completed: cycleDetails.progress_snapshot.completed_issues,
cancelled: cycleDetails.progress_snapshot.cancelled_issues,
}}
totalIssues={cycleDetails.progress_snapshot.total_issues}
isPeekView={Boolean(peekCycle)}
/>
</div>
)}
</>
) : (
<>
{cycleDetails.total_issues > 0 && cycleDetails.distribution && (
<div className="h-full w-full border-t border-neutral-border-medium pt-5">
<SidebarProgressStats
distribution={cycleDetails.distribution}
groupedIssues={{
backlog: cycleDetails.backlog_issues,
unstarted: cycleDetails.unstarted_issues,
started: cycleDetails.started_issues,
completed: cycleDetails.completed_issues,
cancelled: cycleDetails.cancelled_issues,
}}
totalIssues={cycleDetails.total_issues}
isPeekView={Boolean(peekCycle)}
/>
</div>
)}
</>
)}
</div>
</Disclosure.Panel>

View File

@ -33,6 +33,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
const { theme: themStore } = useApplication();
return (
<div className="relative z-10 flex h-[3.75rem] w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-neutral-border-medium bg-sidebar-neutral-component-surface-light p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
@ -50,9 +51,9 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
className="flex flex-grow justify-center text-neutral-text-medium text-sm"
placement="bottom-start"
customButton={
<div className="flex gap-2 items-center px-2 py-1.5 border border-custom-border-400 rounded-md">
<div className="flex gap-2 items-center px-2 py-1.5 border border-neutral-border-strong rounded-md">
<span className="flex flex-grow justify-center text-neutral-text-medium text-sm">{type}</span>
<ChevronDown className="w-4 h-4 text-custom-text-400" />
<ChevronDown className="w-4 h-4 text-neutral-text-subtle" />
</div>
}
customButtonClassName="flex flex-grow justify-center text-neutral-text-medium text-sm"
@ -64,7 +65,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
<Link
key={tab.route}
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
className="text-custom-text-300 w-full"
className="text-neutral-text-medium w-full"
>
{tab.label}
</Link>
@ -75,7 +76,6 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
className="transition-all block md:hidden"
onClick={() => {
themStore.toggleProfileSidebar();
console.log(themStore.profileSidebarCollapsed);
}}
>
<PanelRight

View File

@ -1,13 +1,35 @@
import { useRouter } from "next/router";
import { ArrowLeft, BarChart2 } from "lucide-react";
import { BarChart2, PanelRight } from "lucide-react";
// ui
import { Breadcrumbs } from "@plane/ui";
// components
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
import { BreadcrumbLink } from "components/common";
import { useApplication } from "hooks/store";
import { observer } from "mobx-react";
import { cn } from "helpers/common.helper";
import { useEffect } from "react";
export const WorkspaceAnalyticsHeader = () => {
export const WorkspaceAnalyticsHeader = observer(() => {
const router = useRouter();
const { analytics_tab } = router.query;
const { theme: themeStore } = useApplication();
useEffect(() => {
const handleToggleWorkspaceAnalyticsSidebar = () => {
if (window && window.innerWidth < 768) {
themeStore.toggleWorkspaceAnalyticsSidebar(true);
}
if (window && themeStore.workspaceAnalyticsSidebarCollapsed && window.innerWidth >= 768) {
themeStore.toggleWorkspaceAnalyticsSidebar(false);
}
};
window.addEventListener("resize", handleToggleWorkspaceAnalyticsSidebar);
handleToggleWorkspaceAnalyticsSidebar();
return () => window.removeEventListener("resize", handleToggleWorkspaceAnalyticsSidebar);
}, [themeStore]);
return (
<>
@ -16,7 +38,7 @@ export const WorkspaceAnalyticsHeader = () => {
>
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<SidebarHamburgerToggle />
<div>
<div className="flex items-center justify-between w-full">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
@ -25,9 +47,14 @@ export const WorkspaceAnalyticsHeader = () => {
}
/>
</Breadcrumbs>
{analytics_tab === 'custom' &&
<button className="block md:hidden" onClick={() => { themeStore.toggleWorkspaceAnalyticsSidebar() }}>
<PanelRight className={cn("w-4 h-4 block md:hidden", !themeStore.workspaceAnalyticsSidebarCollapsed ? "text-custom-primary-100" : "text-custom-text-200")} />
</button>
}
</div>
</div>
</div>
</>
);
};
});

View File

@ -1,4 +1,4 @@
import { FC, useCallback, useState } from "react";
import { FC, useCallback, useRef, useState } from "react";
import { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
@ -94,6 +94,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
const { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
// states
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
const [dragState, setDragState] = useState<KanbanDragState>({});
@ -245,8 +247,11 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
</div>
)}
<div className="horizontal-scroll-enable relative h-full w-full overflow-auto">
<div className="relative h-max w-max min-w-full px-2">
<div
className="flex horizontal-scroll-enable relative h-full w-full overflow-auto bg-neutral-component-surface-medium"
ref={scrollableContainerRef}
>
<div className="relative h-max w-max min-w-full bg-neutral-component-surface-medium px-2">
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
{/* drag and delete component */}
<div
@ -289,6 +294,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
canEditProperties={canEditProperties}
storeType={storeType}
addIssuesToView={addIssuesToView}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
</DragDropContext>
</div>

View File

@ -1,4 +1,4 @@
import { memo } from "react";
import { MutableRefObject, memo } from "react";
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
import { observer } from "mobx-react-lite";
// hooks
@ -13,6 +13,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
// helper
import { cn } from "helpers/common.helper";
import RenderIfVisible from "components/core/render-if-visible-HOC";
interface IssueBlockProps {
peekIssueId?: string;
@ -25,6 +26,9 @@ interface IssueBlockProps {
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
}
interface IssueDetailsBlockProps {
@ -107,6 +111,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
handleIssues,
quickActions,
canEditProperties,
scrollableContainerRef,
isDragStarted,
issueIds,
} = props;
const issue = issuesMap[issueId];
@ -129,24 +136,31 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{issue.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-neutral-component-surface-light/20" />
)}
<div
className={cn(
"space-y-2 rounded border-[0.5px] border-neutral-border-medium bg-neutral-component-surface-light px-3 py-2 text-sm transition-all hover:border-neutral-border-strong",
"rounded border-[0.5px] border-neutral-border-medium bg-neutral-component-surface-light px-3 py-2 text-sm transition-all hover:border-neutral-border-strong",
{ "hover:cursor-grab": !isDragDisabled },
{ "border-custom-primary-100": snapshot.isDragging },
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
{ "border-primary-border-subtle": snapshot.isDragging },
{ "border border-primary-border-subtle hover:border-primary-border-subtle": peekIssueId === issue.id }
)}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
<RenderIfVisible
classNames="space-y-2"
root={scrollableContainerRef}
defaultHeight="100px"
horizonatlOffset={50}
alwaysRender={snapshot.isDragging}
pauseHeightUpdateWhileRendering={isDragStarted}
changingReference={issueIds}
>
<KanbanIssueDetailsBlock
issue={issue}
displayProperties={displayProperties}
handleIssues={handleIssues}
quickActions={quickActions}
isReadOnly={!canEditIssueProperties}
/>
</RenderIfVisible>
</div>
</div>
)}

View File

@ -1,4 +1,4 @@
import { memo } from "react";
import { MutableRefObject, memo } from "react";
//types
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
import { EIssueActions } from "../types";
@ -16,6 +16,8 @@ interface IssueBlocksListProps {
handleIssues: (issue: TIssue, action: EIssueActions) => void;
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
@ -30,6 +32,8 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
handleIssues,
quickActions,
canEditProperties,
scrollableContainerRef,
isDragStarted,
} = props;
return (
@ -56,6 +60,9 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
index={index}
isDragDisabled={isDragDisabled}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
/>
);
})}

View File

@ -20,6 +20,7 @@ import {
import { EIssueActions } from "../types";
import { getGroupByColumns } from "../utils";
import { TCreateModalStoreTypes } from "constants/issue";
import { MutableRefObject } from "react";
export interface IGroupByKanBan {
issuesMap: IIssueMap;
@ -45,6 +46,8 @@ export interface IGroupByKanBan {
storeType?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
@ -67,6 +70,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
storeType,
addIssuesToView,
canEditProperties,
scrollableContainerRef,
isDragStarted,
} = props;
const member = useMember();
@ -92,11 +97,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
const groupByVisibilityToggle = visibilityGroupBy(_list);
return (
<div
className={`relative flex flex-shrink-0 flex-col h-full group ${
groupByVisibilityToggle ? `` : `w-[340px]`
}`}
>
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
{sub_group_by === null && (
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-neutral-page-surface-default py-1">
<HeaderGroupByCard
@ -135,6 +136,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
disableIssueCreation={disableIssueCreation}
canEditProperties={canEditProperties}
groupByVisibilityToggle={groupByVisibilityToggle}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
)}
</div>
@ -168,6 +171,8 @@ export interface IKanBan {
storeType?: TCreateModalStoreTypes;
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
@ -189,6 +194,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
storeType,
addIssuesToView,
canEditProperties,
scrollableContainerRef,
isDragStarted,
} = props;
const issueKanBanView = useKanbanView();
@ -213,6 +220,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
storeType={storeType}
addIssuesToView={addIssuesToView}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
);
});

View File

@ -1,3 +1,4 @@
import { MutableRefObject } from "react";
import { Droppable } from "@hello-pangea/dnd";
// hooks
import { useProjectState } from "hooks/store";
@ -37,6 +38,8 @@ interface IKanbanGroup {
disableIssueCreation?: boolean;
canEditProperties: (projectId: string | undefined) => boolean;
groupByVisibilityToggle: boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
isDragStarted?: boolean;
}
export const KanbanGroup = (props: IKanbanGroup) => {
@ -57,6 +60,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
disableIssueCreation,
quickAddCallback,
viewId,
scrollableContainerRef,
isDragStarted,
} = props;
// hooks
const projectState = useProjectState();
@ -127,6 +132,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
{provided.placeholder}

View File

@ -1,3 +1,4 @@
import { MutableRefObject } from "react";
import { observer } from "mobx-react-lite";
// components
import { KanBan } from "./default";
@ -80,6 +81,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
viewId?: string
) => Promise<TIssue | undefined>;
viewId?: string;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
}
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const {
@ -99,6 +101,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
addIssuesToView,
quickAddCallback,
viewId,
scrollableContainerRef,
isDragStarted,
} = props;
const calculateIssueCount = (column_id: string) => {
@ -150,6 +154,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
addIssuesToView={addIssuesToView}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
isDragStarted={isDragStarted}
/>
</div>
)}
@ -183,6 +189,7 @@ export interface IKanBanSwimLanes {
) => Promise<TIssue | undefined>;
viewId?: string;
canEditProperties: (projectId: string | undefined) => boolean;
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
}
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
@ -204,6 +211,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
addIssuesToView,
quickAddCallback,
viewId,
scrollableContainerRef,
} = props;
const member = useMember();
@ -249,6 +257,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
canEditProperties={canEditProperties}
quickAddCallback={quickAddCallback}
viewId={viewId}
scrollableContainerRef={scrollableContainerRef}
/>
)}
</div>

View File

@ -122,26 +122,24 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
);
return (
<>
<div className="relative h-full w-full bg-neutral-page-surface-default">
<List
issuesMap={issueMap}
displayProperties={displayProperties}
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
issueIds={issueIds}
showEmptyGroup={showEmptyGroup}
viewId={viewId}
quickAddCallback={issues?.quickAddIssue}
enableIssueQuickAdd={!!enableQuickAdd}
canEditProperties={canEditProperties}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
storeType={storeType}
addIssuesToView={addIssuesToView}
isCompletedCycle={isCompletedCycle}
/>
</div>
</>
<div className={`relative h-full w-full bg-neutral-page-surface-default`}>
<List
issuesMap={issueMap}
displayProperties={displayProperties}
group_by={group_by}
handleIssues={handleIssues}
quickActions={renderQuickActions}
issueIds={issueIds}
showEmptyGroup={showEmptyGroup}
viewId={viewId}
quickAddCallback={issues?.quickAddIssue}
enableIssueQuickAdd={!!enableQuickAdd}
canEditProperties={canEditProperties}
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
storeType={storeType}
addIssuesToView={addIssuesToView}
isCompletedCycle={isCompletedCycle}
/>
</div>
);
});

View File

@ -48,64 +48,59 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
const projectDetails = getProjectById(issue.project_id);
return (
<>
<div
className={cn(
"relative flex items-center gap-3 bg-neutral-component-surface-light p-3 text-sm border border-transparent border-b-custom-border-200",
{
"border border-custom-primary-70 hover:border-custom-primary-70":
peekIssue && peekIssue.issueId === issue.id,
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
}
)}
>
{displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-neutral-text-medium">
{projectDetails?.identifier}-{issue.sequence_id}
</div>
)}
<div
className={cn("min-h-12 relative flex items-center gap-3 bg-neutral-component-surface-light p-3 text-sm", {
"border border-primary-border-subtle hover:border-primary-border-subtle":
peekIssue && peekIssue.issueId === issue.id,
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
})}
>
{displayProperties && displayProperties?.key && (
<div className="flex-shrink-0 text-xs font-medium text-neutral-text-medium">
{projectDetails?.identifier}-{issue.sequence_id}
</div>
)}
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-neutral-component-surface-light/20" />
)}
{issue?.tempId !== undefined && (
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-neutral-component-surface-light/20" />
)}
{issue?.is_draft ? (
{issue?.is_draft ? (
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
) : (
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-neutral-text-strong"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
) : (
<ControlLink
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issue)}
className="w-full line-clamp-1 cursor-pointer text-sm text-neutral-text-strong"
>
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
<span>{issue.name}</span>
</Tooltip>
</ControlLink>
)}
</ControlLink>
)}
<div className="ml-auto flex flex-shrink-0 items-center gap-2">
{!issue?.tempId ? (
<>
<IssueProperties
className="relative flex items-center gap-2 whitespace-nowrap"
issue={issue}
isReadOnly={!canEditIssueProperties}
handleIssues={updateIssue}
displayProperties={displayProperties}
activeLayout="List"
/>
{quickActions(issue)}
</>
) : (
<div className="h-4 w-4">
<Spinner className="h-4 w-4" />
</div>
)}
</div>
<div className="ml-auto flex flex-shrink-0 items-center gap-2">
{!issue?.tempId ? (
<>
<IssueProperties
className="relative flex items-center gap-2 whitespace-nowrap"
issue={issue}
isReadOnly={!canEditIssueProperties}
handleIssues={updateIssue}
displayProperties={displayProperties}
activeLayout="List"
/>
{quickActions(issue)}
</>
) : (
<div className="h-4 w-4">
<Spinner className="h-4 w-4" />
</div>
)}
</div>
</>
</div>
);
});

View File

@ -1,9 +1,10 @@
import { FC } from "react";
import { FC, MutableRefObject } from "react";
// components
import { IssueBlock } from "components/issues";
// types
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
import { EIssueActions } from "../types";
import RenderIfVisible from "components/core/render-if-visible-HOC";
interface Props {
issueIds: TGroupedIssues | TUnGroupedIssues | any;
@ -12,27 +13,34 @@ interface Props {
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
quickActions: (issue: TIssue) => React.ReactNode;
displayProperties: IIssueDisplayProperties | undefined;
containerRef: MutableRefObject<HTMLDivElement | null>;
}
export const IssueBlocksList: FC<Props> = (props) => {
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props;
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props;
return (
<div className="relative h-full w-full">
{issueIds && issueIds.length > 0 ? (
issueIds.map((issueId: string) => {
if (!issueId) return null;
return (
<IssueBlock
key={issueId}
issueId={issueId}
issuesMap={issuesMap}
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
displayProperties={displayProperties}
/>
<RenderIfVisible
key={`${issueId}`}
defaultHeight="3rem"
root={containerRef}
classNames={"relative border border-transparent border-b-custom-border-200 last:border-b-transparent"}
changingReference={issueIds}
>
<IssueBlock
issueId={issueId}
issuesMap={issuesMap}
handleIssues={handleIssues}
quickActions={quickActions}
canEditProperties={canEditProperties}
displayProperties={displayProperties}
/>
</RenderIfVisible>
);
})
) : (

View File

@ -1,5 +1,7 @@
import { useRef } from "react";
// components
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
import { HeaderGroupByCard } from "./headers/group-by-card";
// hooks
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
// types
@ -10,12 +12,12 @@ import {
IIssueDisplayProperties,
TIssueMap,
TUnGroupedIssues,
IGroupByColumn,
} from "@plane/types";
import { EIssueActions } from "../types";
// constants
import { HeaderGroupByCard } from "./headers/group-by-card";
import { getGroupByColumns } from "../utils";
import { TCreateModalStoreTypes } from "constants/issue";
import { getGroupByColumns } from "../utils";
export interface IGroupByList {
issueIds: TGroupedIssues | TUnGroupedIssues | any;
@ -64,9 +66,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const label = useLabel();
const projectState = useProjectState();
const list = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
const containerRef = useRef<HTMLDivElement | null>(null);
if (!list) return null;
const groups = getGroupByColumns(group_by as GroupByColumnTypes, project, label, projectState, member, true);
if (!groups) return null;
const prePopulateQuickAddData = (groupByKey: string | null, value: any) => {
const defaultState = projectState.projectStates?.find((state) => state.default);
@ -104,11 +108,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
const isGroupByCreatedBy = group_by === "created_by";
return (
<div className="relative h-full w-full">
{list &&
list.length > 0 &&
list.map(
(_list: any) =>
<div ref={containerRef} className="relative overflow-auto h-full w-full">
{groups &&
groups.length > 0 &&
groups.map(
(_list: IGroupByColumn) =>
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
<div key={_list.id} className={`flex flex-shrink-0 flex-col`}>
<div className="sticky top-0 z-[2] w-full flex-shrink-0 border-b border-neutral-border-medium bg-neutral-page-surface-default px-3 py-1">
@ -131,6 +135,7 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
quickActions={quickActions}
displayProperties={displayProperties}
canEditProperties={canEditProperties}
containerRef={containerRef}
/>
)}

View File

@ -1,4 +1,4 @@
import { useRef, useState } from "react";
import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react";
import { useRouter } from "next/router";
import { observer } from "mobx-react-lite";
// icons
@ -7,6 +7,7 @@ import { ChevronRight, MoreHorizontal } from "lucide-react";
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
// components
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
import RenderIfVisible from "components/core/render-if-visible-HOC";
import { IssueColumn } from "./issue-column";
// ui
import { ControlLink, Tooltip } from "@plane/ui";
@ -32,6 +33,9 @@ interface Props {
portalElement: React.MutableRefObject<HTMLDivElement | null>;
nestingLevel: number;
issueId: string;
isScrolled: MutableRefObject<boolean>;
containerRef: MutableRefObject<HTMLTableElement | null>;
issueIds: string[];
}
export const SpreadsheetIssueRow = observer((props: Props) => {
@ -44,8 +48,96 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
handleIssues,
quickActions,
canEditProperties,
isScrolled,
containerRef,
issueIds,
} = props;
const [isExpanded, setExpanded] = useState<boolean>(false);
const { subIssues: subIssuesStore } = useIssueDetail();
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
return (
<>
{/* first column/ issue name and key column */}
<RenderIfVisible
as="tr"
defaultHeight="calc(2.75rem - 1px)"
root={containerRef}
placeholderChildren={<td colSpan={100} className="border-b-[0.5px]" />}
changingReference={issueIds}
>
<IssueRowDetails
issueId={issueId}
displayProperties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties}
nestingLevel={nestingLevel}
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalElement}
isScrolled={isScrolled}
isExpanded={isExpanded}
setExpanded={setExpanded}
/>
</RenderIfVisible>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<SpreadsheetIssueRow
key={subIssueId}
issueId={subIssueId}
displayProperties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties}
nestingLevel={nestingLevel + 1}
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalElement}
isScrolled={isScrolled}
containerRef={containerRef}
issueIds={issueIds}
/>
))}
</>
);
});
interface IssueRowDetailsProps {
displayProperties: IIssueDisplayProperties;
isEstimateEnabled: boolean;
quickActions: (
issue: TIssue,
customActionButton?: React.ReactElement,
portalElement?: HTMLDivElement | null
) => React.ReactNode;
canEditProperties: (projectId: string | undefined) => boolean;
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
portalElement: React.MutableRefObject<HTMLDivElement | null>;
nestingLevel: number;
issueId: string;
isScrolled: MutableRefObject<boolean>;
isExpanded: boolean;
setExpanded: Dispatch<SetStateAction<boolean>>;
}
const IssueRowDetails = observer((props: IssueRowDetailsProps) => {
const {
displayProperties,
issueId,
isEstimateEnabled,
nestingLevel,
portalElement,
handleIssues,
quickActions,
canEditProperties,
isScrolled,
isExpanded,
setExpanded,
} = props;
// router
const router = useRouter();
const { workspaceSlug } = router.query;
@ -54,8 +146,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
const { peekIssue, setPeekIssue } = useIssueDetail();
// states
const [isMenuActive, setIsMenuActive] = useState(false);
const [isExpanded, setExpanded] = useState<boolean>(false);
const menuActionRef = useRef<HTMLDivElement | null>(null);
const handleIssuePeekOverview = (issue: TIssue) => {
@ -66,7 +156,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
const { subIssues: subIssuesStore, issue } = useIssueDetail();
const issueDetail = issue.getIssueById(issueId);
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
const paddingLeft = `${nestingLevel * 54}px`;
@ -91,108 +180,90 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
<MoreHorizontal className="h-3.5 w-3.5" />
</div>
);
if (!issueDetail) return null;
const disableUserActions = !canEditProperties(issueDetail.project_id);
return (
<>
<tr
className={cn({
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issueDetail.id,
})}
<td
className={cn(
"sticky group left-0 h-11 w-[28rem] flex items-center bg-neutral-component-surface-light text-sm after:absolute border-r-[0.5px] border-neutral-border-medium",
{
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
},
{
"border border-primary-border-subtle hover:border-primary-border-subtle":
peekIssue?.issueId === issueDetail.id,
},
{
"shadow-[8px_22px_22px_10px_rgba(0,0,0,0.05)]": isScrolled.current,
}
)}
tabIndex={0}
>
{/* first column/ issue name and key column */}
<td
className={cn(
"sticky group left-0 h-11 w-[28rem] flex items-center bg-neutral-component-surface-light text-sm after:absolute border-r-[0.5px] border-neutral-border-medium focus:border-custom-primary-70",
{
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
}
)}
tabIndex={0}
>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
<div
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
>
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-neutral-text-strong">
<span
className={`flex items-center justify-center font-medium group-hover:opacity-0 ${
isMenuActive ? "opacity-0" : "opacity-100"
}`}
>
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
</span>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
<div
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
>
<div className="relative flex cursor-pointer items-center text-center text-xs hover:text-neutral-text-strong">
<span
className={`flex items-center justify-center font-medium group-hover:opacity-0 ${
isMenuActive ? "opacity-0" : "opacity-100"
}`}
>
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
</span>
{canEditProperties(issueDetail.project_id) && (
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
{quickActions(issueDetail, customActionButton, portalElement.current)}
</div>
)}
</div>
{issueDetail.sub_issues_count > 0 && (
<div className="flex h-6 w-6 items-center justify-center">
<button
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-neutral-component-surface-medium hover:text-neutral-text-strong"
onClick={() => handleToggleExpand()}
>
<ChevronRight className={`h-3.5 w-3.5 ${isExpanded ? "rotate-90" : ""}`} />
</button>
{canEditProperties(issueDetail.project_id) && (
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
{quickActions(issueDetail, customActionButton, portalElement.current)}
</div>
)}
</div>
</WithDisplayPropertiesHOC>
<ControlLink
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issueDetail)}
className="clickable w-full line-clamp-1 cursor-pointer text-sm text-neutral-text-strong"
>
<div className="w-full overflow-hidden">
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
<div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-neutral-text-strong"
tabIndex={-1}
>
{issueDetail.name}
</div>
</Tooltip>
</div>
</ControlLink>
</td>
{/* Rest of the columns */}
{SPREADSHEET_PROPERTY_LIST.map((property) => (
<IssueColumn
displayProperties={displayProperties}
issueDetail={issueDetail}
disableUserActions={disableUserActions}
property={property}
handleIssues={handleIssues}
isEstimateEnabled={isEstimateEnabled}
/>
))}
</tr>
{isExpanded &&
subIssues &&
subIssues.length > 0 &&
subIssues.map((subIssueId: string) => (
<SpreadsheetIssueRow
key={subIssueId}
issueId={subIssueId}
displayProperties={displayProperties}
quickActions={quickActions}
canEditProperties={canEditProperties}
nestingLevel={nestingLevel + 1}
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalElement}
/>
))}
{issueDetail.sub_issues_count > 0 && (
<div className="flex h-6 w-6 items-center justify-center">
<button
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-neutral-component-surface-medium hover:text-neutral-text-strong"
onClick={() => handleToggleExpand()}
>
<ChevronRight className={`h-3.5 w-3.5 ${isExpanded ? "rotate-90" : ""}`} />
</button>
</div>
)}
</div>
</WithDisplayPropertiesHOC>
<ControlLink
href={`/${workspaceSlug}/projects/${issueDetail.project_id}/issues/${issueId}`}
target="_blank"
onClick={() => handleIssuePeekOverview(issueDetail)}
className="clickable w-full line-clamp-1 cursor-pointer text-sm text-neutral-text-strong"
>
<div className="w-full overflow-hidden">
<Tooltip tooltipHeading="Title" tooltipContent={issueDetail.name}>
<div
className="h-full w-full cursor-pointer truncate px-4 py-2.5 text-left text-[0.825rem] text-neutral-text-strong"
tabIndex={-1}
>
{issueDetail.name}
</div>
</Tooltip>
</div>
</ControlLink>
</td>
{/* Rest of the columns */}
{SPREADSHEET_PROPERTY_LIST.map((property) => (
<IssueColumn
displayProperties={displayProperties}
issueDetail={issueDetail}
disableUserActions={disableUserActions}
property={property}
handleIssues={handleIssues}
isEstimateEnabled={isEstimateEnabled}
/>
))}
</>
);
});

View File

@ -1,4 +1,5 @@
import { observer } from "mobx-react-lite";
import { MutableRefObject, useEffect, useRef } from "react";
//types
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
import { EIssueActions } from "../types";
@ -21,6 +22,7 @@ type Props = {
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
canEditProperties: (projectId: string | undefined) => boolean;
portalElement: React.MutableRefObject<HTMLDivElement | null>;
containerRef: MutableRefObject<HTMLTableElement | null>;
};
export const SpreadsheetTable = observer((props: Props) => {
@ -34,8 +36,45 @@ export const SpreadsheetTable = observer((props: Props) => {
quickActions,
handleIssues,
canEditProperties,
containerRef,
} = props;
// states
const isScrolled = useRef(false);
const handleScroll = () => {
if (!containerRef.current) return;
const scrollLeft = containerRef.current.scrollLeft;
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
if (scrollLeft > 0 !== isScrolled.current) {
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
for (let i = 0; i < firtColumns.length; i++) {
const shadow = i === 0 ? headerShadow : columnShadow;
if (scrollLeft > 0) {
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
} else {
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
}
}
isScrolled.current = scrollLeft > 0;
}
};
useEffect(() => {
const currentContainerRef = containerRef.current;
if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll);
return () => {
if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll);
};
}, []);
const handleKeyBoardNavigation = useTableKeyboardNavigation();
return (
@ -58,6 +97,9 @@ export const SpreadsheetTable = observer((props: Props) => {
isEstimateEnabled={isEstimateEnabled}
handleIssues={handleIssues}
portalElement={portalElement}
containerRef={containerRef}
isScrolled={isScrolled}
issueIds={issueIds}
/>
))}
</tbody>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from "react";
import React, { useRef } from "react";
import { observer } from "mobx-react-lite";
// components
import { Spinner } from "@plane/ui";
@ -48,8 +48,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
enableQuickCreateIssue,
disableIssueCreation,
} = props;
// states
const isScrolled = useRef(false);
// refs
const containerRef = useRef<HTMLTableElement | null>(null);
const portalRef = useRef<HTMLDivElement | null>(null);
@ -58,39 +56,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
const isEstimateEnabled: boolean = currentProjectDetails?.estimate !== null;
const handleScroll = () => {
if (!containerRef.current) return;
const scrollLeft = containerRef.current.scrollLeft;
const columnShadow = "8px 22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for regular columns
const headerShadow = "8px -22px 22px 10px rgba(0, 0, 0, 0.05)"; // shadow for headers
//The shadow styles are added this way to avoid re-render of all the rows of table, which could be costly
if (scrollLeft > 0 !== isScrolled.current) {
const firtColumns = containerRef.current.querySelectorAll("table tr td:first-child, th:first-child");
for (let i = 0; i < firtColumns.length; i++) {
const shadow = i === 0 ? headerShadow : columnShadow;
if (scrollLeft > 0) {
(firtColumns[i] as HTMLElement).style.boxShadow = shadow;
} else {
(firtColumns[i] as HTMLElement).style.boxShadow = "none";
}
}
isScrolled.current = scrollLeft > 0;
}
};
useEffect(() => {
const currentContainerRef = containerRef.current;
if (currentContainerRef) currentContainerRef.addEventListener("scroll", handleScroll);
return () => {
if (currentContainerRef) currentContainerRef.removeEventListener("scroll", handleScroll);
};
}, []);
if (!issueIds || issueIds.length === 0)
return (
<div className="grid h-full w-full place-items-center">
@ -112,6 +77,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
quickActions={quickActions}
handleIssues={handleIssues}
canEditProperties={canEditProperties}
containerRef={containerRef}
/>
</div>
<div className="border-t border-neutral-border-subtle">

View File

@ -1,10 +1,10 @@
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ISSUE_PRIORITIES } from "constants/issue";
import { EIssueListRow, ISSUE_PRIORITIES } from "constants/issue";
import { renderEmoji } from "helpers/emoji.helper";
import { IMemberRootStore } from "store/member";
import { IProjectStore } from "store/project/project.store";
import { IStateStore } from "store/state.store";
import { GroupByColumnTypes, IGroupByColumn } from "@plane/types";
import { GroupByColumnTypes, IGroupByColumn, IIssueListRow, TGroupedIssues, TUnGroupedIssues } from "@plane/types";
import { STATE_GROUPS } from "constants/state";
import { ILabelStore } from "store/label.store";

View File

@ -30,7 +30,7 @@ export const ProfileSidebar = observer(() => {
const { workspaceSlug, userId } = router.query;
// store hooks
const { currentUser } = useUser();
const { theme: themStore } = useApplication();
const { theme: themeStore } = useApplication();
const ref = useRef<HTMLDivElement>(null);
const { data: userProjectsData } = useSWR(
@ -41,9 +41,9 @@ export const ProfileSidebar = observer(() => {
);
useOutsideClickDetector(ref, () => {
if (themStore.profileSidebarCollapsed === false) {
if (themeStore.profileSidebarCollapsed === false) {
if (window.innerWidth < 768) {
themStore.toggleProfileSidebar();
themeStore.toggleProfileSidebar();
}
}
});
@ -62,22 +62,22 @@ export const ProfileSidebar = observer(() => {
useEffect(() => {
const handleToggleProfileSidebar = () => {
if (window && window.innerWidth < 768) {
themStore.toggleProfileSidebar(true);
themeStore.toggleProfileSidebar(true);
}
if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) {
themStore.toggleProfileSidebar(false);
if (window && themeStore.profileSidebarCollapsed && window.innerWidth >= 768) {
themeStore.toggleProfileSidebar(false);
}
};
window.addEventListener("resize", handleToggleProfileSidebar);
handleToggleProfileSidebar();
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
}, [themStore]);
}, [themeStore]);
return (
<div
className={`flex-shrink-0 overflow-hidden overflow-y-auto shadow-custom-shadow-sm border-l border-sidebar-neutral-border-subtle bg-sidebar-neutral-component-surface-light h-full z-[5] fixed md:relative transition-all w-full md:w-[300px]`}
style={themStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
style={themeStore.profileSidebarCollapsed ? { marginLeft: `${window?.innerWidth || 0}px` } : {}}
>
{userProjectsData ? (
<>

View File

@ -412,6 +412,13 @@ export const ISSUE_DISPLAY_FILTERS_BY_LAYOUT: {
},
};
export enum EIssueListRow {
HEADER = "HEADER",
ISSUE = "ISSUE",
NO_ISSUES = "NO_ISSUES",
QUICK_ADD = "QUICK_ADD",
}
export const getValueFromObject = (object: Object, key: string): string | number | boolean | null => {
const keys = key ? key.split(".") : [];
@ -442,4 +449,4 @@ export const groupReactionEmojis = (reactions: any) => {
}
return _groupedEmojis;
};
};

View File

@ -15,8 +15,11 @@ import { ANALYTICS_TABS } from "constants/analytics";
import { EUserWorkspaceRoles } from "constants/workspace";
// type
import { NextPageWithLayout } from "lib/types";
import { useRouter } from "next/router";
const AnalyticsPage: NextPageWithLayout = observer(() => {
const router = useRouter();
const { analytics_tab } = router.query;
// theme
const { resolvedTheme } = useTheme();
// store hooks
@ -38,17 +41,25 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
<>
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
<div className="flex h-full flex-col overflow-hidden">
<Tab.Group as={Fragment}>
<Tab.List as="div" className="space-x-2 border-b border-neutral-border-medium px-5 py-3">
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
<Tab.List
as="div"
className="flex space-x-2 border-b border-neutral-border-medium px-0 md:px-5 py-0 md:py-3"
>
{ANALYTICS_TABS.map((tab) => (
<Tab
key={tab.key}
className={({ selected }) =>
`rounded-3xl border border-neutral-border-medium px-4 py-2 text-xs hover:bg-neutral-component-surface-dark ${
selected ? "bg-neutral-component-surface-dark" : ""
`rounded-0 w-full md:w-max md:rounded-3xl border-b md:border border-neutral-border-medium focus:outline-none px-0 md:px-4 py-2 text-xs hover:bg-neutral-component-surface-dark ${
selected
? "border-primary-border-subtle text-primary-text-strong md:bg-neutral-component-surface-dark md:text-neutral-text-medium md:border-neutral-border-medium"
: "border-transparent"
}`
}
onClick={() => {}}
onClick={() => {
router.query.analytics_tab = tab.key;
router.push(router);
}}
>
{tab.title}
</Tab>

View File

@ -8,10 +8,12 @@ export interface IThemeStore {
theme: string | null;
sidebarCollapsed: boolean | undefined;
profileSidebarCollapsed: boolean | undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
// actions
toggleSidebar: (collapsed?: boolean) => void;
setTheme: (theme: any) => void;
toggleProfileSidebar: (collapsed?: boolean) => void;
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
}
export class ThemeStore implements IThemeStore {
@ -19,6 +21,7 @@ export class ThemeStore implements IThemeStore {
sidebarCollapsed: boolean | undefined = undefined;
theme: string | null = null;
profileSidebarCollapsed: boolean | undefined = undefined;
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
// root store
rootStore;
@ -28,10 +31,12 @@ export class ThemeStore implements IThemeStore {
sidebarCollapsed: observable.ref,
theme: observable.ref,
profileSidebarCollapsed: observable.ref,
workspaceAnalyticsSidebarCollapsed: observable.ref,
// action
toggleSidebar: action,
setTheme: action,
toggleProfileSidebar: action,
toggleWorkspaceAnalyticsSidebar: action
// computed
});
// root store
@ -64,6 +69,19 @@ export class ThemeStore implements IThemeStore {
localStorage.setItem("profile_sidebar_collapsed", this.profileSidebarCollapsed.toString());
};
/**
* Toggle the profile sidebar collapsed state
* @param collapsed
*/
toggleWorkspaceAnalyticsSidebar = (collapsed?: boolean) => {
if (collapsed === undefined) {
this.workspaceAnalyticsSidebarCollapsed = !this.workspaceAnalyticsSidebarCollapsed;
} else {
this.workspaceAnalyticsSidebarCollapsed = collapsed;
}
localStorage.setItem("workspace_analytics_sidebar_collapsed", this.workspaceAnalyticsSidebarCollapsed.toString());
};
/**
* Sets the user theme and applies it to the platform
* @param _theme