mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
fix: merge conflicts resolved from develop
This commit is contained in:
commit
14664073ac
@ -20,6 +20,7 @@ from django.core import serializers
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
from django.views.decorators.gzip import gzip_page
|
from django.views.decorators.gzip import gzip_page
|
||||||
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
@ -312,6 +313,7 @@ class CycleViewSet(WebhookMixin, BaseViewSet):
|
|||||||
"labels": label_distribution,
|
"labels": label_distribution,
|
||||||
"completion_chart": {},
|
"completion_chart": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if data[0]["start_date"] and data[0]["end_date"]:
|
if data[0]["start_date"] and data[0]["end_date"]:
|
||||||
data[0]["distribution"][
|
data[0]["distribution"][
|
||||||
"completion_chart"
|
"completion_chart"
|
||||||
@ -840,10 +842,230 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
|||||||
status=status.HTTP_400_BAD_REQUEST,
|
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
|
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 (
|
if (
|
||||||
new_cycle.end_date is not None
|
new_cycle.end_date is not None
|
||||||
and new_cycle.end_date < timezone.now().date()
|
and new_cycle.end_date < timezone.now().date()
|
||||||
|
@ -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),
|
||||||
|
),
|
||||||
|
]
|
@ -68,6 +68,7 @@ class Cycle(ProjectBaseModel):
|
|||||||
sort_order = models.FloatField(default=65535)
|
sort_order = models.FloatField(default=65535)
|
||||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||||
external_id = models.CharField(max_length=255, blank=True, null=True)
|
external_id = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
progress_snapshot = models.JSONField(default=dict)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = "Cycle"
|
verbose_name = "Cycle"
|
||||||
|
18
packages/types/src/cycles.d.ts
vendored
18
packages/types/src/cycles.d.ts
vendored
@ -31,6 +31,7 @@ export interface ICycle {
|
|||||||
issue: string;
|
issue: string;
|
||||||
name: string;
|
name: string;
|
||||||
owned_by: string;
|
owned_by: string;
|
||||||
|
progress_snapshot: TProgressSnapshot;
|
||||||
project: string;
|
project: string;
|
||||||
project_detail: IProjectLite;
|
project_detail: IProjectLite;
|
||||||
status: TCycleGroups;
|
status: TCycleGroups;
|
||||||
@ -49,6 +50,23 @@ export interface ICycle {
|
|||||||
workspace_detail: IWorkspaceLite;
|
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 = {
|
export type TAssigneesDistribution = {
|
||||||
assignee_id: string | null;
|
assignee_id: string | null;
|
||||||
avatar: string | null;
|
avatar: string | null;
|
||||||
|
9
packages/types/src/issues.d.ts
vendored
9
packages/types/src/issues.d.ts
vendored
@ -221,3 +221,12 @@ export interface IGroupByColumn {
|
|||||||
export interface IIssueMap {
|
export interface IIssueMap {
|
||||||
[key: string]: TIssue;
|
[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>;
|
||||||
|
}
|
||||||
|
@ -54,10 +54,6 @@ const CustomMenu = (props: ICustomMenuDropdownProps) => {
|
|||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOnChange = () => {
|
|
||||||
if (closeOnSelect) closeDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectActiveItem = () => {
|
const selectActiveItem = () => {
|
||||||
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
|
const activeItem: HTMLElement | undefined | null = dropdownRef.current?.querySelector(
|
||||||
`[data-headlessui-state="active"] button`
|
`[data-headlessui-state="active"] button`
|
||||||
|
@ -10,6 +10,8 @@ import { CustomAnalyticsSelectBar, CustomAnalyticsMainContent, CustomAnalyticsSi
|
|||||||
import { IAnalyticsParams } from "@plane/types";
|
import { IAnalyticsParams } from "@plane/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ANALYTICS } from "constants/fetch-keys";
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
import { useApplication } from "hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
additionalParams?: Partial<IAnalyticsParams>;
|
additionalParams?: Partial<IAnalyticsParams>;
|
||||||
@ -46,11 +48,13 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
|||||||
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
workspaceSlug ? () => analyticsService.getAnalytics(workspaceSlug.toString(), params) : null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { theme: themeStore } = useApplication();
|
||||||
|
|
||||||
const isProjectLevel = projectId ? true : false;
|
const isProjectLevel = projectId ? true : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col-reverse overflow-hidden ${fullScreen ? "md:grid md:h-full md:grid-cols-4" : ""}`}>
|
<div className={cn("relative w-full h-full flex overflow-hidden", isProjectLevel ? "flex-col-reverse" : "")}>
|
||||||
<div className="col-span-3 flex h-full flex-col overflow-hidden">
|
<div className="w-full flex h-full flex-col overflow-hidden">
|
||||||
<CustomAnalyticsSelectBar
|
<CustomAnalyticsSelectBar
|
||||||
control={control}
|
control={control}
|
||||||
setValue={setValue}
|
setValue={setValue}
|
||||||
@ -61,16 +65,22 @@ export const CustomAnalytics: React.FC<Props> = observer((props) => {
|
|||||||
<CustomAnalyticsMainContent
|
<CustomAnalyticsMainContent
|
||||||
analytics={analytics}
|
analytics={analytics}
|
||||||
error={analyticsError}
|
error={analyticsError}
|
||||||
fullScreen={fullScreen}
|
|
||||||
params={params}
|
params={params}
|
||||||
|
fullScreen={fullScreen}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<CustomAnalyticsSidebar
|
|
||||||
analytics={analytics}
|
<div
|
||||||
params={params}
|
className={cn(
|
||||||
fullScreen={fullScreen}
|
"border-l border-custom-border-200 transition-all",
|
||||||
isProjectLevel={isProjectLevel}
|
!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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -22,9 +22,8 @@ export const CustomAnalyticsSelectBar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid items-center gap-4 px-5 py-2.5 ${isProjectLevel ? "grid-cols-3" : "grid-cols-2"} ${
|
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" : ""
|
||||||
fullScreen ? "md:py-5 lg:grid-cols-4" : ""
|
}`}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{!isProjectLevel && (
|
{!isProjectLevel && (
|
||||||
<div>
|
<div>
|
||||||
|
@ -17,9 +17,9 @@ export const CustomAnalyticsSidebarProjectsList: React.FC<Props> = observer((pro
|
|||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
|
||||||
return (
|
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>
|
<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) => {
|
{projectIds.map((projectId) => {
|
||||||
const project = getProjectById(projectId);
|
const project = getProjectById(projectId);
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
<>
|
<>
|
||||||
{projectId ? (
|
{projectId ? (
|
||||||
cycleDetails ? (
|
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>
|
<h4 className="break-words font-medium">Analytics for {cycleDetails.name}</h4>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
@ -52,7 +52,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : moduleDetails ? (
|
) : 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>
|
<h4 className="break-words font-medium">Analytics for {moduleDetails.name}</h4>
|
||||||
<div className="mt-4 space-y-4">
|
<div className="mt-4 space-y-4">
|
||||||
<div className="flex items-center gap-2 text-xs">
|
<div className="flex items-center gap-2 text-xs">
|
||||||
@ -78,7 +78,7 @@ export const CustomAnalyticsSidebarHeader = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex items-center gap-1">
|
||||||
{projectDetails?.emoji ? (
|
{projectDetails?.emoji ? (
|
||||||
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
|
<div className="grid h-6 w-6 flex-shrink-0 place-items-center">{renderEmoji(projectDetails.emoji)}</div>
|
||||||
|
@ -19,18 +19,18 @@ import { renderFormattedDate } from "helpers/date-time.helper";
|
|||||||
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
|
import { IAnalyticsParams, IAnalyticsResponse, IExportAnalyticsFormData, IWorkspace } from "@plane/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ANALYTICS } from "constants/fetch-keys";
|
import { ANALYTICS } from "constants/fetch-keys";
|
||||||
|
import { cn } from "helpers/common.helper";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
analytics: IAnalyticsResponse | undefined;
|
analytics: IAnalyticsResponse | undefined;
|
||||||
params: IAnalyticsParams;
|
params: IAnalyticsParams;
|
||||||
fullScreen: boolean;
|
|
||||||
isProjectLevel: boolean;
|
isProjectLevel: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const analyticsService = new AnalyticsService();
|
const analyticsService = new AnalyticsService();
|
||||||
|
|
||||||
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { analytics, params, fullScreen, isProjectLevel = false } = props;
|
const { analytics, params, isProjectLevel = false } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -140,16 +140,16 @@ export const CustomAnalyticsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-between space-y-2 px-5 py-2.5 ${
|
className={cn(
|
||||||
fullScreen
|
"relative h-full flex w-full gap-2 justify-between items-start px-5 py-4 bg-sidebar-neutral-component-surface-light",
|
||||||
? "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"
|
!isProjectLevel ? "flex-col" : ""
|
||||||
: ""
|
)}
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<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">
|
<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} />
|
<LayersIcon height={14} width={14} />
|
||||||
{analytics ? analytics.total : "..."} Issues
|
{analytics ? analytics.total : "..."}{" "}
|
||||||
|
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Issues</div>
|
||||||
</div>
|
</div>
|
||||||
{isProjectLevel && (
|
{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">
|
<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>
|
</div>
|
||||||
<div className="h-full w-full overflow-hidden">
|
|
||||||
{fullScreen ? (
|
<div className={cn("h-full w-full overflow-hidden", isProjectLevel ? "hidden" : "block")}>
|
||||||
<>
|
<>
|
||||||
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
{!isProjectLevel && selectedProjects && selectedProjects.length > 0 && (
|
||||||
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
<CustomAnalyticsSidebarProjectsList projectIds={selectedProjects} />
|
||||||
)}
|
)}
|
||||||
<CustomAnalyticsSidebarHeader />
|
<CustomAnalyticsSidebarHeader />
|
||||||
</>
|
</>
|
||||||
) : null}
|
|
||||||
</div>
|
</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
|
<Button
|
||||||
variant="outline-neutral"
|
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={() => {
|
onClick={() => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
mutate(ANALYTICS(workspaceSlug.toString(), params));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Refresh
|
<div className={cn(isProjectLevel ? "hidden md:block" : "")}>Refresh</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="primary" prependIcon={<Download className="h-3.5 w-3.5" />} onClick={exportAnalytics}>
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,13 +20,15 @@ export const ProjectAnalyticsModalMainContent: React.FC<Props> = observer((props
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Group as={React.Fragment}>
|
<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) => (
|
{ANALYTICS_TABS.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-3xl border border-neutral-border-medium px-4 py-2 text-xs hover: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 ? "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={() => {}}
|
onClick={() => {}}
|
||||||
|
80
web/components/core/render-if-visible-HOC.tsx
Normal file
80
web/components/core/render-if-visible-HOC.tsx
Normal 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;
|
@ -3,6 +3,7 @@ import { useRouter } from "next/router";
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
import { Disclosure, Popover, Transition } from "@headlessui/react";
|
||||||
|
import isEmpty from "lodash/isEmpty";
|
||||||
// services
|
// services
|
||||||
import { CycleService } from "services/cycle.service";
|
import { CycleService } from "services/cycle.service";
|
||||||
// hooks
|
// 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 isEndValid = new Date(`${cycleDetails?.end_date}`) >= new Date(`${cycleDetails?.start_date}`);
|
||||||
|
|
||||||
const progressPercentage = cycleDetails
|
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;
|
: null;
|
||||||
|
|
||||||
if (!cycleDetails)
|
if (!cycleDetails)
|
||||||
@ -317,7 +322,14 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
const currentCycle = CYCLE_STATUS.find((status) => status.value === cycleStatus);
|
||||||
|
|
||||||
const issueCount =
|
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 daysLeft = findHowManyDaysLeft(cycleDetails.end_date);
|
||||||
|
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER;
|
||||||
@ -568,49 +580,105 @@ export const CycleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<Transition show={open}>
|
<Transition show={open}>
|
||||||
<Disclosure.Panel>
|
<Disclosure.Panel>
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{cycleDetails.distribution?.completion_chart &&
|
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
|
||||||
cycleDetails.start_date &&
|
<>
|
||||||
cycleDetails.end_date ? (
|
{cycleDetails.progress_snapshot.distribution?.completion_chart &&
|
||||||
<div className="h-full w-full pt-4">
|
cycleDetails.start_date &&
|
||||||
<div className="flex items-start gap-4 py-2 text-xs">
|
cycleDetails.end_date && (
|
||||||
<div className="flex items-center gap-3 text-neutral-text-strong">
|
<div className="h-full w-full pt-4">
|
||||||
<div className="flex items-center justify-center gap-1">
|
<div className="flex items-start gap-4 py-2 text-xs">
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-[#A9BBD0]" />
|
<div className="flex items-center gap-3 text-neutral-text-strong">
|
||||||
<span>Ideal</span>
|
<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>
|
||||||
<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 && (
|
{/* stats */}
|
||||||
<div className="h-full w-full border-t border-neutral-border-medium pt-5">
|
{isCompleted && !isEmpty(cycleDetails.progress_snapshot) ? (
|
||||||
<SidebarProgressStats
|
<>
|
||||||
distribution={cycleDetails.distribution}
|
{cycleDetails.progress_snapshot.total_issues > 0 &&
|
||||||
groupedIssues={{
|
cycleDetails.progress_snapshot.distribution && (
|
||||||
backlog: cycleDetails.backlog_issues,
|
<div className="h-full w-full border-t border-neutral-border-medium pt-5">
|
||||||
unstarted: cycleDetails.unstarted_issues,
|
<SidebarProgressStats
|
||||||
started: cycleDetails.started_issues,
|
distribution={cycleDetails.progress_snapshot.distribution}
|
||||||
completed: cycleDetails.completed_issues,
|
groupedIssues={{
|
||||||
cancelled: cycleDetails.cancelled_issues,
|
backlog: cycleDetails.progress_snapshot.backlog_issues,
|
||||||
}}
|
unstarted: cycleDetails.progress_snapshot.unstarted_issues,
|
||||||
totalIssues={cycleDetails.total_issues}
|
started: cycleDetails.progress_snapshot.started_issues,
|
||||||
isPeekView={Boolean(peekCycle)}
|
completed: cycleDetails.progress_snapshot.completed_issues,
|
||||||
/>
|
cancelled: cycleDetails.progress_snapshot.cancelled_issues,
|
||||||
</div>
|
}}
|
||||||
|
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>
|
</div>
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
|
@ -33,6 +33,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
|||||||
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
const tabsList = isAuthorized ? [...PROFILE_VIEWER_TAB, ...PROFILE_ADMINS_TAB] : PROFILE_VIEWER_TAB;
|
||||||
|
|
||||||
const { theme: themStore } = useApplication();
|
const { theme: themStore } = useApplication();
|
||||||
|
|
||||||
return (
|
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="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">
|
<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"
|
className="flex flex-grow justify-center text-neutral-text-medium text-sm"
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
customButton={
|
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>
|
<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>
|
</div>
|
||||||
}
|
}
|
||||||
customButtonClassName="flex flex-grow justify-center text-neutral-text-medium text-sm"
|
customButtonClassName="flex flex-grow justify-center text-neutral-text-medium text-sm"
|
||||||
@ -64,7 +65,7 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
|||||||
<Link
|
<Link
|
||||||
key={tab.route}
|
key={tab.route}
|
||||||
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
|
href={`/${workspaceSlug}/profile/${userId}/${tab.route}`}
|
||||||
className="text-custom-text-300 w-full"
|
className="text-neutral-text-medium w-full"
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</Link>
|
</Link>
|
||||||
@ -75,7 +76,6 @@ export const UserProfileHeader: FC<TUserProfileHeader> = observer((props) => {
|
|||||||
className="transition-all block md:hidden"
|
className="transition-all block md:hidden"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
themStore.toggleProfileSidebar();
|
themStore.toggleProfileSidebar();
|
||||||
console.log(themStore.profileSidebarCollapsed);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PanelRight
|
<PanelRight
|
||||||
|
@ -1,13 +1,35 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ArrowLeft, BarChart2 } from "lucide-react";
|
import { BarChart2, PanelRight } from "lucide-react";
|
||||||
// ui
|
// ui
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle";
|
||||||
import { BreadcrumbLink } from "components/common";
|
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 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -16,7 +38,7 @@ export const WorkspaceAnalyticsHeader = () => {
|
|||||||
>
|
>
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<SidebarHamburgerToggle />
|
<SidebarHamburgerToggle />
|
||||||
<div>
|
<div className="flex items-center justify-between w-full">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
<Breadcrumbs.BreadcrumbItem
|
<Breadcrumbs.BreadcrumbItem
|
||||||
type="text"
|
type="text"
|
||||||
@ -25,9 +47,14 @@ export const WorkspaceAnalyticsHeader = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Breadcrumbs>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -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 { DragDropContext, DragStart, DraggableLocation, DropResult, Droppable } from "@hello-pangea/dnd";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
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 { enableInlineEditing, enableQuickAdd, enableIssueCreation } = issues?.viewFlags || {};
|
||||||
|
|
||||||
|
const scrollableContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
// states
|
// states
|
||||||
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
const [isDragStarted, setIsDragStarted] = useState<boolean>(false);
|
||||||
const [dragState, setDragState] = useState<KanbanDragState>({});
|
const [dragState, setDragState] = useState<KanbanDragState>({});
|
||||||
@ -245,8 +247,11 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="horizontal-scroll-enable relative h-full w-full overflow-auto">
|
<div
|
||||||
<div className="relative h-max w-max min-w-full px-2">
|
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}>
|
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||||
{/* drag and delete component */}
|
{/* drag and delete component */}
|
||||||
<div
|
<div
|
||||||
@ -289,6 +294,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo } from "react";
|
import { MutableRefObject, memo } from "react";
|
||||||
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
import { Draggable, DraggableProvided, DraggableStateSnapshot } from "@hello-pangea/dnd";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
@ -13,6 +13,7 @@ import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
|||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "helpers/common.helper";
|
import { cn } from "helpers/common.helper";
|
||||||
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
peekIssueId?: string;
|
peekIssueId?: string;
|
||||||
@ -25,6 +26,9 @@ interface IssueBlockProps {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue) => React.ReactNode;
|
quickActions: (issue: TIssue) => React.ReactNode;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
|
issueIds: string[]; //DO NOT REMOVE< needed to force render for virtualization
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IssueDetailsBlockProps {
|
interface IssueDetailsBlockProps {
|
||||||
@ -107,6 +111,9 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
|
issueIds,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issue = issuesMap[issueId];
|
const issue = issuesMap[issueId];
|
||||||
@ -129,24 +136,31 @@ export const KanbanIssueBlock: React.FC<IssueBlockProps> = memo((props) => {
|
|||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
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
|
<div
|
||||||
className={cn(
|
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 },
|
{ "hover:cursor-grab": !isDragDisabled },
|
||||||
{ "border-custom-primary-100": snapshot.isDragging },
|
{ "border-primary-border-subtle": snapshot.isDragging },
|
||||||
{ "border border-custom-primary-70 hover:border-custom-primary-70": peekIssueId === issue.id }
|
{ "border border-primary-border-subtle hover:border-primary-border-subtle": peekIssueId === issue.id }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<KanbanIssueDetailsBlock
|
<RenderIfVisible
|
||||||
issue={issue}
|
classNames="space-y-2"
|
||||||
displayProperties={displayProperties}
|
root={scrollableContainerRef}
|
||||||
handleIssues={handleIssues}
|
defaultHeight="100px"
|
||||||
quickActions={quickActions}
|
horizonatlOffset={50}
|
||||||
isReadOnly={!canEditIssueProperties}
|
alwaysRender={snapshot.isDragging}
|
||||||
/>
|
pauseHeightUpdateWhileRendering={isDragStarted}
|
||||||
|
changingReference={issueIds}
|
||||||
|
>
|
||||||
|
<KanbanIssueDetailsBlock
|
||||||
|
issue={issue}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
|
isReadOnly={!canEditIssueProperties}
|
||||||
|
/>
|
||||||
|
</RenderIfVisible>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { memo } from "react";
|
import { MutableRefObject, memo } from "react";
|
||||||
//types
|
//types
|
||||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
@ -16,6 +16,8 @@ interface IssueBlocksListProps {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
||||||
@ -30,6 +32,8 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -56,6 +60,9 @@ const KanbanIssueBlocksListMemo: React.FC<IssueBlocksListProps> = (props) => {
|
|||||||
index={index}
|
index={index}
|
||||||
isDragDisabled={isDragDisabled}
|
isDragDisabled={isDragDisabled}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
|
issueIds={issueIds} //passing to force render for virtualization whenever parent rerenders
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
import { getGroupByColumns } from "../utils";
|
import { getGroupByColumns } from "../utils";
|
||||||
import { TCreateModalStoreTypes } from "constants/issue";
|
import { TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { MutableRefObject } from "react";
|
||||||
|
|
||||||
export interface IGroupByKanBan {
|
export interface IGroupByKanBan {
|
||||||
issuesMap: IIssueMap;
|
issuesMap: IIssueMap;
|
||||||
@ -45,6 +46,8 @@ export interface IGroupByKanBan {
|
|||||||
storeType?: TCreateModalStoreTypes;
|
storeType?: TCreateModalStoreTypes;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
||||||
@ -67,6 +70,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
storeType,
|
storeType,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -92,11 +97,7 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={`relative flex flex-shrink-0 flex-col group ${groupByVisibilityToggle ? `` : `w-[340px]`}`}>
|
||||||
className={`relative flex flex-shrink-0 flex-col h-full group ${
|
|
||||||
groupByVisibilityToggle ? `` : `w-[340px]`
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-neutral-page-surface-default py-1">
|
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-neutral-page-surface-default py-1">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
@ -135,6 +136,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
groupByVisibilityToggle={groupByVisibilityToggle}
|
groupByVisibilityToggle={groupByVisibilityToggle}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -168,6 +171,8 @@ export interface IKanBan {
|
|||||||
storeType?: TCreateModalStoreTypes;
|
storeType?: TCreateModalStoreTypes;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
addIssuesToView?: (issueIds: string[]) => Promise<TIssue>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
export const KanBan: React.FC<IKanBan> = observer((props) => {
|
||||||
@ -189,6 +194,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
storeType,
|
storeType,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
@ -213,6 +220,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
storeType={storeType}
|
storeType={storeType}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MutableRefObject } from "react";
|
||||||
import { Droppable } from "@hello-pangea/dnd";
|
import { Droppable } from "@hello-pangea/dnd";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProjectState } from "hooks/store";
|
import { useProjectState } from "hooks/store";
|
||||||
@ -37,6 +38,8 @@ interface IKanbanGroup {
|
|||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
groupByVisibilityToggle: boolean;
|
groupByVisibilityToggle: boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
|
isDragStarted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanGroup = (props: IKanbanGroup) => {
|
export const KanbanGroup = (props: IKanbanGroup) => {
|
||||||
@ -57,6 +60,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
// hooks
|
// hooks
|
||||||
const projectState = useProjectState();
|
const projectState = useProjectState();
|
||||||
@ -127,6 +132,8 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { MutableRefObject } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
@ -80,6 +81,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
|||||||
viewId?: string
|
viewId?: string
|
||||||
) => Promise<TIssue | undefined>;
|
) => Promise<TIssue | undefined>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
@ -99,6 +101,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
scrollableContainerRef,
|
||||||
|
isDragStarted,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const calculateIssueCount = (column_id: string) => {
|
const calculateIssueCount = (column_id: string) => {
|
||||||
@ -150,6 +154,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
|
isDragStarted={isDragStarted}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -183,6 +189,7 @@ export interface IKanBanSwimLanes {
|
|||||||
) => Promise<TIssue | undefined>;
|
) => Promise<TIssue | undefined>;
|
||||||
viewId?: string;
|
viewId?: string;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
|
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
||||||
@ -204,6 +211,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
scrollableContainerRef,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const member = useMember();
|
const member = useMember();
|
||||||
@ -249,6 +257,7 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
|
scrollableContainerRef={scrollableContainerRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -122,26 +122,24 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={`relative h-full w-full bg-neutral-page-surface-default`}>
|
||||||
<div className="relative h-full w-full bg-neutral-page-surface-default">
|
<List
|
||||||
<List
|
issuesMap={issueMap}
|
||||||
issuesMap={issueMap}
|
displayProperties={displayProperties}
|
||||||
displayProperties={displayProperties}
|
group_by={group_by}
|
||||||
group_by={group_by}
|
handleIssues={handleIssues}
|
||||||
handleIssues={handleIssues}
|
quickActions={renderQuickActions}
|
||||||
quickActions={renderQuickActions}
|
issueIds={issueIds}
|
||||||
issueIds={issueIds}
|
showEmptyGroup={showEmptyGroup}
|
||||||
showEmptyGroup={showEmptyGroup}
|
viewId={viewId}
|
||||||
viewId={viewId}
|
quickAddCallback={issues?.quickAddIssue}
|
||||||
quickAddCallback={issues?.quickAddIssue}
|
enableIssueQuickAdd={!!enableQuickAdd}
|
||||||
enableIssueQuickAdd={!!enableQuickAdd}
|
canEditProperties={canEditProperties}
|
||||||
canEditProperties={canEditProperties}
|
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
||||||
disableIssueCreation={!enableIssueCreation || !isEditingAllowed}
|
storeType={storeType}
|
||||||
storeType={storeType}
|
addIssuesToView={addIssuesToView}
|
||||||
addIssuesToView={addIssuesToView}
|
isCompletedCycle={isCompletedCycle}
|
||||||
isCompletedCycle={isCompletedCycle}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -48,64 +48,59 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
const projectDetails = getProjectById(issue.project_id);
|
const projectDetails = getProjectById(issue.project_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className={cn("min-h-12 relative flex items-center gap-3 bg-neutral-component-surface-light p-3 text-sm", {
|
||||||
className={cn(
|
"border border-primary-border-subtle hover:border-primary-border-subtle":
|
||||||
"relative flex items-center gap-3 bg-neutral-component-surface-light p-3 text-sm border border-transparent border-b-custom-border-200",
|
peekIssue && peekIssue.issueId === issue.id,
|
||||||
{
|
"last:border-b-transparent": peekIssue?.issueId !== issue.id,
|
||||||
"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>
|
||||||
{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 && (
|
{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="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}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
</ControlLink>
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="ml-auto flex flex-shrink-0 items-center gap-2">
|
<div className="ml-auto flex flex-shrink-0 items-center gap-2">
|
||||||
{!issue?.tempId ? (
|
{!issue?.tempId ? (
|
||||||
<>
|
<>
|
||||||
<IssueProperties
|
<IssueProperties
|
||||||
className="relative flex items-center gap-2 whitespace-nowrap"
|
className="relative flex items-center gap-2 whitespace-nowrap"
|
||||||
issue={issue}
|
issue={issue}
|
||||||
isReadOnly={!canEditIssueProperties}
|
isReadOnly={!canEditIssueProperties}
|
||||||
handleIssues={updateIssue}
|
handleIssues={updateIssue}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
activeLayout="List"
|
activeLayout="List"
|
||||||
/>
|
/>
|
||||||
{quickActions(issue)}
|
{quickActions(issue)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-4 w-4">
|
<div className="h-4 w-4">
|
||||||
<Spinner className="h-4 w-4" />
|
<Spinner className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { FC } from "react";
|
import { FC, MutableRefObject } from "react";
|
||||||
// components
|
// components
|
||||||
import { IssueBlock } from "components/issues";
|
import { IssueBlock } from "components/issues";
|
||||||
// types
|
// types
|
||||||
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
import { TGroupedIssues, TIssue, IIssueDisplayProperties, TIssueMap, TUnGroupedIssues } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||||
@ -12,27 +13,34 @@ interface Props {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
quickActions: (issue: TIssue) => React.ReactNode;
|
quickActions: (issue: TIssue) => React.ReactNode;
|
||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueBlocksList: FC<Props> = (props) => {
|
export const IssueBlocksList: FC<Props> = (props) => {
|
||||||
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties } = props;
|
const { issueIds, issuesMap, handleIssues, quickActions, displayProperties, canEditProperties, containerRef } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
{issueIds && issueIds.length > 0 ? (
|
{issueIds && issueIds.length > 0 ? (
|
||||||
issueIds.map((issueId: string) => {
|
issueIds.map((issueId: string) => {
|
||||||
if (!issueId) return null;
|
if (!issueId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssueBlock
|
<RenderIfVisible
|
||||||
key={issueId}
|
key={`${issueId}`}
|
||||||
issueId={issueId}
|
defaultHeight="3rem"
|
||||||
issuesMap={issuesMap}
|
root={containerRef}
|
||||||
handleIssues={handleIssues}
|
classNames={"relative border border-transparent border-b-custom-border-200 last:border-b-transparent"}
|
||||||
quickActions={quickActions}
|
changingReference={issueIds}
|
||||||
canEditProperties={canEditProperties}
|
>
|
||||||
displayProperties={displayProperties}
|
<IssueBlock
|
||||||
/>
|
issueId={issueId}
|
||||||
|
issuesMap={issuesMap}
|
||||||
|
handleIssues={handleIssues}
|
||||||
|
quickActions={quickActions}
|
||||||
|
canEditProperties={canEditProperties}
|
||||||
|
displayProperties={displayProperties}
|
||||||
|
/>
|
||||||
|
</RenderIfVisible>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
// components
|
// components
|
||||||
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
|
import { IssueBlocksList, ListQuickAddIssueForm } from "components/issues";
|
||||||
|
import { HeaderGroupByCard } from "./headers/group-by-card";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
import { useLabel, useMember, useProject, useProjectState } from "hooks/store";
|
||||||
// types
|
// types
|
||||||
@ -10,12 +12,12 @@ import {
|
|||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
TIssueMap,
|
TIssueMap,
|
||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
|
IGroupByColumn,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
// constants
|
// constants
|
||||||
import { HeaderGroupByCard } from "./headers/group-by-card";
|
|
||||||
import { getGroupByColumns } from "../utils";
|
|
||||||
import { TCreateModalStoreTypes } from "constants/issue";
|
import { TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
import { getGroupByColumns } from "../utils";
|
||||||
|
|
||||||
export interface IGroupByList {
|
export interface IGroupByList {
|
||||||
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
issueIds: TGroupedIssues | TUnGroupedIssues | any;
|
||||||
@ -64,9 +66,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
|||||||
const label = useLabel();
|
const label = useLabel();
|
||||||
const projectState = useProjectState();
|
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 prePopulateQuickAddData = (groupByKey: string | null, value: any) => {
|
||||||
const defaultState = projectState.projectStates?.find((state) => state.default);
|
const defaultState = projectState.projectStates?.find((state) => state.default);
|
||||||
@ -104,11 +108,11 @@ const GroupByList: React.FC<IGroupByList> = (props) => {
|
|||||||
const isGroupByCreatedBy = group_by === "created_by";
|
const isGroupByCreatedBy = group_by === "created_by";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
<div ref={containerRef} className="relative overflow-auto h-full w-full">
|
||||||
{list &&
|
{groups &&
|
||||||
list.length > 0 &&
|
groups.length > 0 &&
|
||||||
list.map(
|
groups.map(
|
||||||
(_list: any) =>
|
(_list: IGroupByColumn) =>
|
||||||
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
|
validateEmptyIssueGroups(is_list ? issueIds : issueIds?.[_list.id]) && (
|
||||||
<div key={_list.id} className={`flex flex-shrink-0 flex-col`}>
|
<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">
|
<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}
|
quickActions={quickActions}
|
||||||
displayProperties={displayProperties}
|
displayProperties={displayProperties}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
containerRef={containerRef}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { Dispatch, MutableRefObject, SetStateAction, useRef, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// icons
|
// icons
|
||||||
@ -7,6 +7,7 @@ import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|||||||
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
import { SPREADSHEET_PROPERTY_LIST } from "constants/spreadsheet";
|
||||||
// components
|
// components
|
||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
|
import RenderIfVisible from "components/core/render-if-visible-HOC";
|
||||||
import { IssueColumn } from "./issue-column";
|
import { IssueColumn } from "./issue-column";
|
||||||
// ui
|
// ui
|
||||||
import { ControlLink, Tooltip } from "@plane/ui";
|
import { ControlLink, Tooltip } from "@plane/ui";
|
||||||
@ -32,6 +33,9 @@ interface Props {
|
|||||||
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
nestingLevel: number;
|
nestingLevel: number;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
isScrolled: MutableRefObject<boolean>;
|
||||||
|
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||||
|
issueIds: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SpreadsheetIssueRow = observer((props: Props) => {
|
export const SpreadsheetIssueRow = observer((props: Props) => {
|
||||||
@ -44,8 +48,96 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
isScrolled,
|
||||||
|
containerRef,
|
||||||
|
issueIds,
|
||||||
} = props;
|
} = 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
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug } = router.query;
|
const { workspaceSlug } = router.query;
|
||||||
@ -54,8 +146,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
const { peekIssue, setPeekIssue } = useIssueDetail();
|
const { peekIssue, setPeekIssue } = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
const [isExpanded, setExpanded] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: TIssue) => {
|
const handleIssuePeekOverview = (issue: TIssue) => {
|
||||||
@ -66,7 +156,6 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
const { subIssues: subIssuesStore, issue } = useIssueDetail();
|
||||||
|
|
||||||
const issueDetail = issue.getIssueById(issueId);
|
const issueDetail = issue.getIssueById(issueId);
|
||||||
const subIssues = subIssuesStore.subIssuesByIssueId(issueId);
|
|
||||||
|
|
||||||
const paddingLeft = `${nestingLevel * 54}px`;
|
const paddingLeft = `${nestingLevel * 54}px`;
|
||||||
|
|
||||||
@ -91,108 +180,90 @@ export const SpreadsheetIssueRow = observer((props: Props) => {
|
|||||||
<MoreHorizontal className="h-3.5 w-3.5" />
|
<MoreHorizontal className="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!issueDetail) return null;
|
if (!issueDetail) return null;
|
||||||
|
|
||||||
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
const disableUserActions = !canEditProperties(issueDetail.project_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<tr
|
<td
|
||||||
className={cn({
|
className={cn(
|
||||||
"border border-custom-primary-70 hover:border-custom-primary-70": peekIssue?.issueId === issueDetail.id,
|
"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 */}
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
||||||
<td
|
<div
|
||||||
className={cn(
|
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
|
||||||
"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",
|
style={issueDetail.parent_id && nestingLevel !== 0 ? { paddingLeft } : {}}
|
||||||
{
|
>
|
||||||
"border-b-[0.5px]": peekIssue?.issueId !== issueDetail.id,
|
<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 ${
|
||||||
tabIndex={0}
|
isMenuActive ? "opacity-0" : "opacity-100"
|
||||||
>
|
}`}
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="key">
|
>
|
||||||
<div
|
{getProjectById(issueDetail.project_id)?.identifier}-{issueDetail.sequence_id}
|
||||||
className="flex min-w-min items-center gap-1.5 px-4 py-2.5 pr-0"
|
</span>
|
||||||
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) && (
|
{canEditProperties(issueDetail.project_id) && (
|
||||||
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
<div className={`absolute left-2.5 top-0 hidden group-hover:block ${isMenuActive ? "!block" : ""}`}>
|
||||||
{quickActions(issueDetail, customActionButton, portalElement.current)}
|
{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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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 &&
|
{issueDetail.sub_issues_count > 0 && (
|
||||||
subIssues &&
|
<div className="flex h-6 w-6 items-center justify-center">
|
||||||
subIssues.length > 0 &&
|
<button
|
||||||
subIssues.map((subIssueId: string) => (
|
className="h-5 w-5 cursor-pointer rounded-sm hover:bg-neutral-component-surface-medium hover:text-neutral-text-strong"
|
||||||
<SpreadsheetIssueRow
|
onClick={() => handleToggleExpand()}
|
||||||
key={subIssueId}
|
>
|
||||||
issueId={subIssueId}
|
<ChevronRight className={`h-3.5 w-3.5 ${isExpanded ? "rotate-90" : ""}`} />
|
||||||
displayProperties={displayProperties}
|
</button>
|
||||||
quickActions={quickActions}
|
</div>
|
||||||
canEditProperties={canEditProperties}
|
)}
|
||||||
nestingLevel={nestingLevel + 1}
|
</div>
|
||||||
isEstimateEnabled={isEstimateEnabled}
|
</WithDisplayPropertiesHOC>
|
||||||
handleIssues={handleIssues}
|
<ControlLink
|
||||||
portalElement={portalElement}
|
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}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { MutableRefObject, useEffect, useRef } from "react";
|
||||||
//types
|
//types
|
||||||
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
|
import { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssue } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
@ -21,6 +22,7 @@ type Props = {
|
|||||||
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
handleIssues: (issue: TIssue, action: EIssueActions) => Promise<void>;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
portalElement: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
|
containerRef: MutableRefObject<HTMLTableElement | null>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SpreadsheetTable = observer((props: Props) => {
|
export const SpreadsheetTable = observer((props: Props) => {
|
||||||
@ -34,8 +36,45 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
quickActions,
|
quickActions,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
|
containerRef,
|
||||||
} = props;
|
} = 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();
|
const handleKeyBoardNavigation = useTableKeyboardNavigation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,6 +97,9 @@ export const SpreadsheetTable = observer((props: Props) => {
|
|||||||
isEstimateEnabled={isEstimateEnabled}
|
isEstimateEnabled={isEstimateEnabled}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
portalElement={portalElement}
|
portalElement={portalElement}
|
||||||
|
containerRef={containerRef}
|
||||||
|
isScrolled={isScrolled}
|
||||||
|
issueIds={issueIds}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// components
|
// components
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
@ -48,8 +48,6 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
enableQuickCreateIssue,
|
enableQuickCreateIssue,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
|
||||||
const isScrolled = useRef(false);
|
|
||||||
// refs
|
// refs
|
||||||
const containerRef = useRef<HTMLTableElement | null>(null);
|
const containerRef = useRef<HTMLTableElement | null>(null);
|
||||||
const portalRef = useRef<HTMLDivElement | 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 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)
|
if (!issueIds || issueIds.length === 0)
|
||||||
return (
|
return (
|
||||||
<div className="grid h-full w-full place-items-center">
|
<div className="grid h-full w-full place-items-center">
|
||||||
@ -112,6 +77,7 @@ export const SpreadsheetView: React.FC<Props> = observer((props) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
|
containerRef={containerRef}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-neutral-border-subtle">
|
<div className="border-t border-neutral-border-subtle">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { Avatar, PriorityIcon, StateGroupIcon } from "@plane/ui";
|
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 { renderEmoji } from "helpers/emoji.helper";
|
||||||
import { IMemberRootStore } from "store/member";
|
import { IMemberRootStore } from "store/member";
|
||||||
import { IProjectStore } from "store/project/project.store";
|
import { IProjectStore } from "store/project/project.store";
|
||||||
import { IStateStore } from "store/state.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 { STATE_GROUPS } from "constants/state";
|
||||||
import { ILabelStore } from "store/label.store";
|
import { ILabelStore } from "store/label.store";
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ export const ProfileSidebar = observer(() => {
|
|||||||
const { workspaceSlug, userId } = router.query;
|
const { workspaceSlug, userId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
const { theme: themStore } = useApplication();
|
const { theme: themeStore } = useApplication();
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const { data: userProjectsData } = useSWR(
|
const { data: userProjectsData } = useSWR(
|
||||||
@ -41,9 +41,9 @@ export const ProfileSidebar = observer(() => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useOutsideClickDetector(ref, () => {
|
useOutsideClickDetector(ref, () => {
|
||||||
if (themStore.profileSidebarCollapsed === false) {
|
if (themeStore.profileSidebarCollapsed === false) {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
themStore.toggleProfileSidebar();
|
themeStore.toggleProfileSidebar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -62,22 +62,22 @@ export const ProfileSidebar = observer(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleToggleProfileSidebar = () => {
|
const handleToggleProfileSidebar = () => {
|
||||||
if (window && window.innerWidth < 768) {
|
if (window && window.innerWidth < 768) {
|
||||||
themStore.toggleProfileSidebar(true);
|
themeStore.toggleProfileSidebar(true);
|
||||||
}
|
}
|
||||||
if (window && themStore.profileSidebarCollapsed && window.innerWidth >= 768) {
|
if (window && themeStore.profileSidebarCollapsed && window.innerWidth >= 768) {
|
||||||
themStore.toggleProfileSidebar(false);
|
themeStore.toggleProfileSidebar(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener("resize", handleToggleProfileSidebar);
|
window.addEventListener("resize", handleToggleProfileSidebar);
|
||||||
handleToggleProfileSidebar();
|
handleToggleProfileSidebar();
|
||||||
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
|
return () => window.removeEventListener("resize", handleToggleProfileSidebar);
|
||||||
}, [themStore]);
|
}, [themeStore]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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]`}
|
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 ? (
|
{userProjectsData ? (
|
||||||
<>
|
<>
|
||||||
|
@ -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 => {
|
export const getValueFromObject = (object: Object, key: string): string | number | boolean | null => {
|
||||||
const keys = key ? key.split(".") : [];
|
const keys = key ? key.split(".") : [];
|
||||||
|
|
||||||
@ -442,4 +449,4 @@ export const groupReactionEmojis = (reactions: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return _groupedEmojis;
|
return _groupedEmojis;
|
||||||
};
|
};
|
@ -15,8 +15,11 @@ import { ANALYTICS_TABS } from "constants/analytics";
|
|||||||
import { EUserWorkspaceRoles } from "constants/workspace";
|
import { EUserWorkspaceRoles } from "constants/workspace";
|
||||||
// type
|
// type
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
const AnalyticsPage: NextPageWithLayout = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { analytics_tab } = router.query;
|
||||||
// theme
|
// theme
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
// store hooks
|
// store hooks
|
||||||
@ -38,17 +41,25 @@ const AnalyticsPage: NextPageWithLayout = observer(() => {
|
|||||||
<>
|
<>
|
||||||
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
{workspaceProjectIds && workspaceProjectIds.length > 0 ? (
|
||||||
<div className="flex h-full flex-col overflow-hidden">
|
<div className="flex h-full flex-col overflow-hidden">
|
||||||
<Tab.Group as={Fragment}>
|
<Tab.Group as={Fragment} defaultIndex={analytics_tab === "custom" ? 1 : 0}>
|
||||||
<Tab.List as="div" className="space-x-2 border-b border-neutral-border-medium px-5 py-3">
|
<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) => (
|
{ANALYTICS_TABS.map((tab) => (
|
||||||
<Tab
|
<Tab
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
className={({ selected }) =>
|
||||||
`rounded-3xl border border-neutral-border-medium px-4 py-2 text-xs hover: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 ? "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.title}
|
||||||
</Tab>
|
</Tab>
|
||||||
|
@ -8,10 +8,12 @@ export interface IThemeStore {
|
|||||||
theme: string | null;
|
theme: string | null;
|
||||||
sidebarCollapsed: boolean | undefined;
|
sidebarCollapsed: boolean | undefined;
|
||||||
profileSidebarCollapsed: boolean | undefined;
|
profileSidebarCollapsed: boolean | undefined;
|
||||||
|
workspaceAnalyticsSidebarCollapsed: boolean | undefined;
|
||||||
// actions
|
// actions
|
||||||
toggleSidebar: (collapsed?: boolean) => void;
|
toggleSidebar: (collapsed?: boolean) => void;
|
||||||
setTheme: (theme: any) => void;
|
setTheme: (theme: any) => void;
|
||||||
toggleProfileSidebar: (collapsed?: boolean) => void;
|
toggleProfileSidebar: (collapsed?: boolean) => void;
|
||||||
|
toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ThemeStore implements IThemeStore {
|
export class ThemeStore implements IThemeStore {
|
||||||
@ -19,6 +21,7 @@ export class ThemeStore implements IThemeStore {
|
|||||||
sidebarCollapsed: boolean | undefined = undefined;
|
sidebarCollapsed: boolean | undefined = undefined;
|
||||||
theme: string | null = null;
|
theme: string | null = null;
|
||||||
profileSidebarCollapsed: boolean | undefined = undefined;
|
profileSidebarCollapsed: boolean | undefined = undefined;
|
||||||
|
workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined;
|
||||||
// root store
|
// root store
|
||||||
rootStore;
|
rootStore;
|
||||||
|
|
||||||
@ -28,10 +31,12 @@ export class ThemeStore implements IThemeStore {
|
|||||||
sidebarCollapsed: observable.ref,
|
sidebarCollapsed: observable.ref,
|
||||||
theme: observable.ref,
|
theme: observable.ref,
|
||||||
profileSidebarCollapsed: observable.ref,
|
profileSidebarCollapsed: observable.ref,
|
||||||
|
workspaceAnalyticsSidebarCollapsed: observable.ref,
|
||||||
// action
|
// action
|
||||||
toggleSidebar: action,
|
toggleSidebar: action,
|
||||||
setTheme: action,
|
setTheme: action,
|
||||||
toggleProfileSidebar: action,
|
toggleProfileSidebar: action,
|
||||||
|
toggleWorkspaceAnalyticsSidebar: action
|
||||||
// computed
|
// computed
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
@ -64,6 +69,19 @@ export class ThemeStore implements IThemeStore {
|
|||||||
localStorage.setItem("profile_sidebar_collapsed", this.profileSidebarCollapsed.toString());
|
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
|
* Sets the user theme and applies it to the platform
|
||||||
* @param _theme
|
* @param _theme
|
||||||
|
Loading…
Reference in New Issue
Block a user