mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
[WEB-412] chore: estimates analytics (#4730)
* chore: estimate points in modules and cycle * chore: burn down chart analytics * chore: module serializer change * dev: handled y-axis estimates in analytics, implemented estimate points on modules * chore: burn down analytics * chore: state estimate point analytics * chore: updated the burn down values * Remove check mark from estimate point edit field in create estimate flow --------- Co-authored-by: guru_sainath <gurusainath007@gmail.com> Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
This commit is contained in:
parent
23f4d5418f
commit
dae291fe3b
@ -784,6 +784,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||
plot_type = request.GET.get("plot_type", "issues")
|
||||
|
||||
if not new_cycle_id:
|
||||
return Response(
|
||||
@ -865,6 +866,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
|
||||
queryset=old_cycle.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
cycle_id=cycle_id,
|
||||
)
|
||||
|
||||
|
@ -177,6 +177,8 @@ class ModuleSerializer(DynamicBaseSerializer):
|
||||
started_issues = serializers.IntegerField(read_only=True)
|
||||
unstarted_issues = serializers.IntegerField(read_only=True)
|
||||
backlog_issues = serializers.IntegerField(read_only=True)
|
||||
total_estimate_points = serializers.IntegerField(read_only=True)
|
||||
completed_estimate_points = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Module
|
||||
@ -201,6 +203,8 @@ class ModuleSerializer(DynamicBaseSerializer):
|
||||
"external_id",
|
||||
"logo_props",
|
||||
# computed fields
|
||||
"total_estimate_points",
|
||||
"completed_estimate_points",
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"cancelled_issues",
|
||||
|
@ -177,6 +177,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
plot_type = request.GET.get("plot_type", "issues")
|
||||
if pk is None:
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
@ -375,6 +376,7 @@ class CycleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
queryset=queryset,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
cycle_id=pk,
|
||||
)
|
||||
|
||||
|
@ -17,8 +17,11 @@ from django.db.models import (
|
||||
UUIDField,
|
||||
Value,
|
||||
When,
|
||||
Subquery,
|
||||
Sum,
|
||||
IntegerField,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.utils import timezone
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
@ -73,6 +76,89 @@ class CycleViewSet(BaseViewSet):
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
backlog_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="backlog",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
backlog_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("backlog_estimate_point")[:1]
|
||||
)
|
||||
unstarted_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="unstarted",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
unstarted_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("unstarted_estimate_point")[:1]
|
||||
)
|
||||
started_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="started",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
started_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("started_estimate_point")[:1]
|
||||
)
|
||||
cancelled_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="cancelled",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
cancelled_estimate_point=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("cancelled_estimate_point")[:1]
|
||||
)
|
||||
completed_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
total_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_cycle__cycle_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_cycle__cycle_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
@ -197,12 +283,49 @@ class CycleViewSet(BaseViewSet):
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
backlog_estimate_points=Coalesce(
|
||||
Subquery(backlog_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
unstarted_estimate_points=Coalesce(
|
||||
Subquery(unstarted_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
started_estimate_points=Coalesce(
|
||||
Subquery(started_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
cancelled_estimate_points=Coalesce(
|
||||
Subquery(cancelled_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.order_by("-is_favorite", "name")
|
||||
.distinct()
|
||||
)
|
||||
|
||||
def list(self, request, slug, project_id):
|
||||
queryset = self.get_queryset().filter(archived_at__isnull=True)
|
||||
plot_type = request.GET.get("plot_type", "issues")
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
|
||||
# Update the order by
|
||||
@ -233,6 +356,12 @@ class CycleViewSet(BaseViewSet):
|
||||
"progress_snapshot",
|
||||
"logo_props",
|
||||
# meta fields
|
||||
"backlog_estimate_points",
|
||||
"unstarted_estimate_points",
|
||||
"started_estimate_points",
|
||||
"cancelled_estimate_points",
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"cancelled_issues",
|
||||
@ -335,6 +464,7 @@ class CycleViewSet(BaseViewSet):
|
||||
queryset=queryset.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
cycle_id=data[0]["id"],
|
||||
)
|
||||
)
|
||||
@ -359,6 +489,8 @@ class CycleViewSet(BaseViewSet):
|
||||
"progress_snapshot",
|
||||
"logo_props",
|
||||
# meta fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"total_issues",
|
||||
"cancelled_issues",
|
||||
@ -527,6 +659,7 @@ class CycleViewSet(BaseViewSet):
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
plot_type = request.GET.get("plot_type", "issues")
|
||||
queryset = (
|
||||
self.get_queryset().filter(archived_at__isnull=True).filter(pk=pk)
|
||||
)
|
||||
@ -682,6 +815,7 @@ class CycleViewSet(BaseViewSet):
|
||||
queryset=queryset,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
cycle_id=pk,
|
||||
)
|
||||
|
||||
@ -798,6 +932,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
|
||||
def post(self, request, slug, project_id, cycle_id):
|
||||
new_cycle_id = request.data.get("new_cycle_id", False)
|
||||
plot_type = request.GET.get("plot_type", "issues")
|
||||
|
||||
if not new_cycle_id:
|
||||
return Response(
|
||||
@ -879,6 +1014,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
|
||||
queryset=old_cycle.first(),
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
cycle_id=cycle_id,
|
||||
)
|
||||
|
||||
|
@ -165,6 +165,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
)
|
||||
|
||||
def get(self, request, slug, project_id, pk=None):
|
||||
plot_type = request.GET.get("plot_type", "issues")
|
||||
if pk is None:
|
||||
queryset = self.get_queryset()
|
||||
modules = queryset.values( # Required fields
|
||||
@ -323,6 +324,7 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
|
||||
queryset=modules,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
module_id=pk,
|
||||
)
|
||||
|
||||
|
@ -16,8 +16,9 @@ from django.db.models import (
|
||||
Subquery,
|
||||
UUIDField,
|
||||
Value,
|
||||
Sum,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.db.models.functions import Coalesce, Cast
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.utils import timezone
|
||||
|
||||
@ -128,6 +129,34 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(cnt=Count("pk"))
|
||||
.values("cnt")
|
||||
)
|
||||
completed_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
state__group="completed",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
completed_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("completed_estimate_points")[:1]
|
||||
)
|
||||
|
||||
total_estimate_point = (
|
||||
Issue.issue_objects.filter(
|
||||
estimate_point__estimate__type="points",
|
||||
issue_module__module_id=OuterRef("pk"),
|
||||
)
|
||||
.values("issue_module__module_id")
|
||||
.annotate(
|
||||
total_estimate_points=Sum(
|
||||
Cast("estimate_point__value", IntegerField())
|
||||
)
|
||||
)
|
||||
.values("total_estimate_points")[:1]
|
||||
)
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
@ -182,6 +211,18 @@ class ModuleViewSet(BaseViewSet):
|
||||
Value(0, output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
completed_estimate_points=Coalesce(
|
||||
Subquery(completed_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
total_estimate_points=Coalesce(
|
||||
Subquery(total_estimate_point),
|
||||
Value(0, output_field=IntegerField()),
|
||||
),
|
||||
)
|
||||
.annotate(
|
||||
member_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
@ -233,6 +274,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
"total_issues",
|
||||
"started_issues",
|
||||
"unstarted_issues",
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"backlog_issues",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
@ -284,6 +327,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
"external_id",
|
||||
"logo_props",
|
||||
# computed fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"total_issues",
|
||||
"is_favorite",
|
||||
"cancelled_issues",
|
||||
@ -301,6 +346,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
return Response(modules, status=status.HTTP_200_OK)
|
||||
|
||||
def retrieve(self, request, slug, project_id, pk):
|
||||
plot_type = request.GET.get("plot_type", "burndown")
|
||||
queryset = (
|
||||
self.get_queryset()
|
||||
.filter(archived_at__isnull=True)
|
||||
@ -423,6 +469,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
queryset=modules,
|
||||
slug=slug,
|
||||
project_id=project_id,
|
||||
plot_type=plot_type,
|
||||
module_id=pk,
|
||||
)
|
||||
|
||||
@ -469,6 +516,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
"external_id",
|
||||
"logo_props",
|
||||
# computed fields
|
||||
"completed_estimate_points",
|
||||
"total_estimate_points",
|
||||
"is_favorite",
|
||||
"cancelled_issues",
|
||||
"completed_issues",
|
||||
|
@ -4,18 +4,28 @@ from itertools import groupby
|
||||
|
||||
# Django import
|
||||
from django.db import models
|
||||
from django.db.models import Case, CharField, Count, F, Sum, Value, When
|
||||
from django.db.models import (
|
||||
Case,
|
||||
CharField,
|
||||
Count,
|
||||
F,
|
||||
Sum,
|
||||
Value,
|
||||
When,
|
||||
IntegerField,
|
||||
)
|
||||
from django.db.models.functions import (
|
||||
Coalesce,
|
||||
Concat,
|
||||
ExtractMonth,
|
||||
ExtractYear,
|
||||
TruncDate,
|
||||
Cast,
|
||||
)
|
||||
from django.utils import timezone
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue
|
||||
from plane.db.models import Issue, Project
|
||||
|
||||
|
||||
def annotate_with_monthly_dimension(queryset, field_name, attribute):
|
||||
@ -87,9 +97,9 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
||||
|
||||
# Estimate
|
||||
else:
|
||||
queryset = queryset.annotate(estimate=Sum("estimate_point")).order_by(
|
||||
x_axis
|
||||
)
|
||||
queryset = queryset.annotate(
|
||||
estimate=Sum(Cast("estimate_point__value", IntegerField()))
|
||||
).order_by(x_axis)
|
||||
queryset = (
|
||||
queryset.annotate(segment=F(segment)) if segment else queryset
|
||||
)
|
||||
@ -110,9 +120,33 @@ def build_graph_plot(queryset, x_axis, y_axis, segment=None):
|
||||
return sort_data(grouped_data, temp_axis)
|
||||
|
||||
|
||||
def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
def burndown_plot(
|
||||
queryset,
|
||||
slug,
|
||||
project_id,
|
||||
plot_type,
|
||||
cycle_id=None,
|
||||
module_id=None,
|
||||
):
|
||||
# Total Issues in Cycle or Module
|
||||
total_issues = queryset.total_issues
|
||||
# check whether the estimate is a point or not
|
||||
estimate_type = Project.objects.filter(
|
||||
workspace__slug=slug,
|
||||
pk=project_id,
|
||||
estimate__isnull=False,
|
||||
estimate__type="points",
|
||||
).exists()
|
||||
if estimate_type and plot_type == "points":
|
||||
issue_estimates = Issue.objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
estimate_point__isnull=False,
|
||||
).values_list("estimate_point__value", flat=True)
|
||||
|
||||
issue_estimates = [int(value) for value in issue_estimates]
|
||||
total_estimate_points = sum(issue_estimates)
|
||||
|
||||
if cycle_id:
|
||||
if queryset.end_date and queryset.start_date:
|
||||
@ -128,18 +162,32 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
|
||||
chart_data = {str(date): 0 for date in date_range}
|
||||
|
||||
completed_issues_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
if plot_type == "points":
|
||||
completed_issues_estimate_point_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
estimate_point__isnull=False,
|
||||
)
|
||||
.annotate(date=TruncDate("completed_at"))
|
||||
.values("date")
|
||||
.values("date", "estimate_point__value")
|
||||
.order_by("date")
|
||||
)
|
||||
else:
|
||||
completed_issues_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_cycle__cycle_id=cycle_id,
|
||||
)
|
||||
.annotate(date=TruncDate("completed_at"))
|
||||
.values("date")
|
||||
.annotate(total_completed=Count("id"))
|
||||
.values("date", "total_completed")
|
||||
.order_by("date")
|
||||
)
|
||||
.annotate(date=TruncDate("completed_at"))
|
||||
.values("date")
|
||||
.annotate(total_completed=Count("id"))
|
||||
.values("date", "total_completed")
|
||||
.order_by("date")
|
||||
)
|
||||
|
||||
if module_id:
|
||||
# Get all dates between the two dates
|
||||
@ -152,31 +200,59 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
|
||||
|
||||
chart_data = {str(date): 0 for date in date_range}
|
||||
|
||||
completed_issues_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_module__module_id=module_id,
|
||||
if plot_type == "points":
|
||||
completed_issues_estimate_point_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_module__module_id=module_id,
|
||||
estimate_point__isnull=False,
|
||||
)
|
||||
.annotate(date=TruncDate("completed_at"))
|
||||
.values("date")
|
||||
.values("date", "estimate_point__value")
|
||||
.order_by("date")
|
||||
)
|
||||
else:
|
||||
completed_issues_distribution = (
|
||||
Issue.issue_objects.filter(
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
issue_module__module_id=module_id,
|
||||
)
|
||||
.annotate(date=TruncDate("completed_at"))
|
||||
.values("date")
|
||||
.annotate(total_completed=Count("id"))
|
||||
.values("date", "total_completed")
|
||||
.order_by("date")
|
||||
)
|
||||
.annotate(date=TruncDate("completed_at"))
|
||||
.values("date")
|
||||
.annotate(total_completed=Count("id"))
|
||||
.values("date", "total_completed")
|
||||
.order_by("date")
|
||||
)
|
||||
|
||||
for date in date_range:
|
||||
cumulative_pending_issues = total_issues
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
item["total_completed"]
|
||||
for item in completed_issues_distribution
|
||||
if item["date"] is not None and item["date"] <= date
|
||||
)
|
||||
cumulative_pending_issues -= total_completed
|
||||
if date > timezone.now().date():
|
||||
chart_data[str(date)] = None
|
||||
if plot_type == "points":
|
||||
cumulative_pending_issues = total_estimate_points
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
int(item["estimate_point__value"])
|
||||
for item in completed_issues_estimate_point_distribution
|
||||
if item["date"] is not None and item["date"] <= date
|
||||
)
|
||||
cumulative_pending_issues -= total_completed
|
||||
if date > timezone.now().date():
|
||||
chart_data[str(date)] = None
|
||||
else:
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
else:
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
cumulative_pending_issues = total_issues
|
||||
total_completed = 0
|
||||
total_completed = sum(
|
||||
item["total_completed"]
|
||||
for item in completed_issues_distribution
|
||||
if item["date"] is not None and item["date"] <= date
|
||||
)
|
||||
cumulative_pending_issues -= total_completed
|
||||
if date > timezone.now().date():
|
||||
chart_data[str(date)] = None
|
||||
else:
|
||||
chart_data[str(date)] = cumulative_pending_issues
|
||||
|
||||
return chart_data
|
||||
|
2
packages/types/src/module/modules.d.ts
vendored
2
packages/types/src/module/modules.d.ts
vendored
@ -44,6 +44,8 @@ export interface IModule {
|
||||
target_date: string | null;
|
||||
total_issues: number;
|
||||
unstarted_issues: number;
|
||||
total_estimate_points?: number;
|
||||
completed_estimate_points?: number;
|
||||
updated_at: string;
|
||||
updated_by?: string;
|
||||
archived_at: string | null;
|
||||
|
@ -1,26 +1,54 @@
|
||||
// ui
|
||||
import { observer } from "mobx-react";
|
||||
import { TYAxisValues } from "@plane/types";
|
||||
import { CustomSelect } from "@plane/ui";
|
||||
// types
|
||||
import { ANALYTICS_Y_AXIS_VALUES } from "@/constants/analytics";
|
||||
// constants
|
||||
import { ANALYTICS_Y_AXIS_VALUES } from "@/constants/analytics";
|
||||
import { EEstimateSystem } from "@/constants/estimates";
|
||||
// hooks
|
||||
import { useAppRouter, useProjectEstimates } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
value: TYAxisValues;
|
||||
onChange: () => void;
|
||||
};
|
||||
|
||||
export const SelectYAxis: React.FC<Props> = ({ value, onChange }) => (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={<span>{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}</span>}
|
||||
onChange={onChange}
|
||||
maxHeight="lg"
|
||||
>
|
||||
{ANALYTICS_Y_AXIS_VALUES.map((item) => (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
))}
|
||||
</CustomSelect>
|
||||
);
|
||||
export const SelectYAxis: React.FC<Props> = observer(({ value, onChange }) => {
|
||||
// hooks
|
||||
const { projectId } = useAppRouter();
|
||||
const { areEstimateEnabledByProjectId, currentActiveEstimateId, estimateById } = useProjectEstimates();
|
||||
|
||||
const isEstimateEnabled = (analyticsOption: string) => {
|
||||
if (analyticsOption === "estimate") {
|
||||
if (
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS
|
||||
) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomSelect
|
||||
value={value}
|
||||
label={<span>{ANALYTICS_Y_AXIS_VALUES.find((v) => v.value === value)?.label ?? "None"}</span>}
|
||||
onChange={onChange}
|
||||
maxHeight="lg"
|
||||
>
|
||||
{ANALYTICS_Y_AXIS_VALUES.map(
|
||||
(item) =>
|
||||
isEstimateEnabled(item.value) && (
|
||||
<CustomSelect.Option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</CustomSelect.Option>
|
||||
)
|
||||
)}
|
||||
</CustomSelect>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, FormEvent, useState } from "react";
|
||||
import { FC, MouseEvent, FocusEvent, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, Info, X } from "lucide-react";
|
||||
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
||||
@ -49,7 +49,7 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
||||
closeCallBack();
|
||||
};
|
||||
|
||||
const handleCreate = async (event: FormEvent<HTMLFormElement>) => {
|
||||
const handleCreate = async (event: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement, Element>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
@ -122,10 +122,10 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleCreate} className="relative flex items-center gap-2 text-base">
|
||||
<form className="relative flex items-center gap-2 text-base">
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full border rounded flex items-center",
|
||||
"relative w-full border rounded flex items-center my-1",
|
||||
error ? `border-red-500` : `border-custom-border-200`
|
||||
)}
|
||||
>
|
||||
@ -136,6 +136,7 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
||||
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||
placeholder="Enter estimate point"
|
||||
autoFocus
|
||||
onBlur={(e) => !estimateId && handleCreate(e)}
|
||||
/>
|
||||
{error && (
|
||||
<>
|
||||
@ -148,20 +149,25 @@ export const EstimatePointCreate: FC<TEstimatePointCreate> = observer((props) =>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
||||
disabled={loader}
|
||||
>
|
||||
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||
onClick={handleClose}
|
||||
disabled={loader}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</button>
|
||||
{estimateId && (
|
||||
<>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
||||
disabled={loader}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||
onClick={handleClose}
|
||||
disabled={loader}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FC, FormEvent, useEffect, useState } from "react";
|
||||
import { FC, MouseEvent, useEffect, FocusEvent, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, Info, X } from "lucide-react";
|
||||
import { TEstimatePointsObject, TEstimateSystemKeys } from "@plane/types";
|
||||
@ -57,7 +57,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
||||
closeCallBack();
|
||||
};
|
||||
|
||||
const handleUpdate = async (event: FormEvent<HTMLFormElement>) => {
|
||||
const handleUpdate = async (event: MouseEvent<HTMLButtonElement> | FocusEvent<HTMLInputElement, Element>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
@ -134,10 +134,10 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleUpdate} className="relative flex items-center gap-2 text-base">
|
||||
<form className="relative flex items-center gap-2 text-base">
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full border rounded flex items-center",
|
||||
"relative w-full border rounded flex items-center my-1",
|
||||
error ? `border-red-500` : `border-custom-border-200`
|
||||
)}
|
||||
>
|
||||
@ -147,6 +147,7 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
||||
onChange={(e) => setEstimateInputValue(e.target.value)}
|
||||
className="border-none focus:ring-0 focus:border-0 focus:outline-none p-2.5 w-full bg-transparent"
|
||||
placeholder="Enter estimate point"
|
||||
onBlur={(e) => !estimateId && handleUpdate(e)}
|
||||
autoFocus
|
||||
/>
|
||||
{error && (
|
||||
@ -159,21 +160,25 @@ export const EstimatePointUpdate: FC<TEstimatePointUpdate> = observer((props) =>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
||||
disabled={loader}
|
||||
>
|
||||
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||
onClick={handleClose}
|
||||
disabled={loader}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</button>
|
||||
{estimateId && (
|
||||
<>
|
||||
<button
|
||||
type="submit"
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer text-green-500"
|
||||
disabled={loader}
|
||||
onClick={handleUpdate}
|
||||
>
|
||||
{loader ? <Spinner className="w-4 h-4" /> : <Check size={14} />}
|
||||
</button>
|
||||
<button
|
||||
className="rounded-sm w-6 h-6 flex-shrink-0 relative flex justify-center items-center hover:bg-custom-background-80 transition-colors cursor-pointer"
|
||||
onClick={handleClose}
|
||||
disabled={loader}
|
||||
>
|
||||
<X size={14} className="text-custom-text-200" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
});
|
||||
|
@ -10,13 +10,14 @@ import { FavoriteStar } from "@/components/core";
|
||||
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
|
||||
import { ModuleQuickActions } from "@/components/modules";
|
||||
// constants
|
||||
import { EEstimateSystem } from "@/constants/estimates";
|
||||
import { MODULE_FAVORITED, MODULE_UNFAVORITED } from "@/constants/event-tracker";
|
||||
import { MODULE_STATUS } from "@/constants/module";
|
||||
import { EUserProjectRoles } from "@/constants/project";
|
||||
// helpers
|
||||
import { getDate, renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useMember, useModule, useUser } from "@/hooks/store";
|
||||
import { useEventTracker, useMember, useModule, useProjectEstimates, useUser } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
@ -37,6 +38,8 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
const { getModuleById, addModuleToFavorites, removeModuleFromFavorites } = useModule();
|
||||
const { getUserDetails } = useMember();
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
@ -120,14 +123,30 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
const moduleTotalIssues =
|
||||
moduleDetails.backlog_issues +
|
||||
moduleDetails.unstarted_issues +
|
||||
moduleDetails.started_issues +
|
||||
moduleDetails.completed_issues +
|
||||
moduleDetails.cancelled_issues;
|
||||
/**
|
||||
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||
*/
|
||||
const isEstimateEnabled =
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId.toString()) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const completionPercentage = (moduleDetails.completed_issues / moduleTotalIssues) * 100;
|
||||
const moduleTotalIssues = isEstimateEnabled
|
||||
? moduleDetails?.total_estimate_points || 0
|
||||
: moduleDetails.backlog_issues +
|
||||
moduleDetails.unstarted_issues +
|
||||
moduleDetails.started_issues +
|
||||
moduleDetails.completed_issues +
|
||||
moduleDetails.cancelled_issues;
|
||||
|
||||
const moduleCompletedIssues = isEstimateEnabled
|
||||
? moduleDetails?.completed_estimate_points || 0
|
||||
: moduleDetails.completed_issues;
|
||||
|
||||
const completionPercentage = (moduleCompletedIssues / moduleTotalIssues) * 100;
|
||||
|
||||
const endDate = getDate(moduleDetails.target_date);
|
||||
const startDate = getDate(moduleDetails.start_date);
|
||||
@ -140,11 +159,11 @@ export const ModuleCardItem: React.FC<Props> = observer((props) => {
|
||||
|
||||
const issueCount = module
|
||||
? !moduleTotalIssues || moduleTotalIssues === 0
|
||||
? "0 Issue"
|
||||
: moduleTotalIssues === moduleDetails.completed_issues
|
||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}`
|
||||
: `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues`
|
||||
: "0 Issue";
|
||||
? `0 ${isEstimateEnabled ? `Point` : `Issue`}`
|
||||
: moduleTotalIssues === moduleCompletedIssues
|
||||
? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? `s` : ``}`
|
||||
: `${moduleCompletedIssues}/${moduleTotalIssues} ${isEstimateEnabled ? `Points` : `Issues`}`
|
||||
: `0 ${isEstimateEnabled ? `Point` : `Issue`}`;
|
||||
|
||||
const moduleLeadDetails = moduleDetails.lead_id ? getUserDetails(moduleDetails.lead_id) : undefined;
|
||||
|
||||
|
@ -8,8 +8,10 @@ import { CircularProgressIndicator } from "@plane/ui";
|
||||
// components
|
||||
import { ListItem } from "@/components/core/list";
|
||||
import { ModuleListItemAction } from "@/components/modules";
|
||||
// constants
|
||||
import { EEstimateSystem } from "@/constants/estimates";
|
||||
// hooks
|
||||
import { useModule } from "@/hooks/store";
|
||||
import { useAppRouter, useModule, useProjectEstimates } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type Props = {
|
||||
@ -26,14 +28,28 @@ export const ModuleListItem: React.FC<Props> = observer((props) => {
|
||||
// store hooks
|
||||
const { getModuleById } = useModule();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { projectId } = useAppRouter();
|
||||
const { currentActiveEstimateId, areEstimateEnabledByProjectId, estimateById } = useProjectEstimates();
|
||||
|
||||
// derived values
|
||||
const moduleDetails = getModuleById(moduleId);
|
||||
|
||||
if (!moduleDetails) return null;
|
||||
|
||||
const completionPercentage =
|
||||
((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||
/**
|
||||
* NOTE: This completion percentage calculation is based on the total issues count.
|
||||
* when estimates are available and estimate type is points, we should consider the estimate point count
|
||||
* when estimates are available and estimate type is not points, then by default we consider the issue count
|
||||
*/
|
||||
const isEstimateEnabled =
|
||||
projectId &&
|
||||
currentActiveEstimateId &&
|
||||
areEstimateEnabledByProjectId(projectId) &&
|
||||
estimateById(currentActiveEstimateId)?.type === EEstimateSystem.POINTS;
|
||||
|
||||
const completionPercentage = isEstimateEnabled
|
||||
? ((moduleDetails?.completed_estimate_points || 0) / (moduleDetails?.total_estimate_points || 0)) * 100
|
||||
: ((moduleDetails.completed_issues + moduleDetails.cancelled_issues) / moduleDetails.total_issues) * 100;
|
||||
|
||||
const progress = isNaN(completionPercentage) ? 0 : Math.floor(completionPercentage);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user