UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: трехсекционная шкала прогресса чекеров
This commit is contained in:
parent
5f9d9c418e
commit
aa348d5d64
|
|
@ -11,6 +11,7 @@ from .state import StateLiteSerializer
|
||||||
from .user import UserLiteSerializer
|
from .user import UserLiteSerializer
|
||||||
from plane.app.serializers.issue import LabelSerializer
|
from plane.app.serializers.issue import LabelSerializer
|
||||||
from plane.db.models import FileAsset, IntakeIssue, Issue, IssueActivity, IssueComment, Label, Notification, Project
|
from plane.db.models import FileAsset, IntakeIssue, Issue, IssueActivity, IssueComment, Label, Notification, Project
|
||||||
|
from plane.utils.issue_checker_progress import build_issue_checker_progress
|
||||||
|
|
||||||
|
|
||||||
class ExternalContourIssuePayloadSerializer(serializers.Serializer):
|
class ExternalContourIssuePayloadSerializer(serializers.Serializer):
|
||||||
|
|
@ -86,6 +87,9 @@ class ExternalContourIssueSerializer(BaseSerializer):
|
||||||
label_ids = serializers.SerializerMethodField()
|
label_ids = serializers.SerializerMethodField()
|
||||||
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
project_detail = ProjectLiteSerializer(source="project", read_only=True)
|
||||||
state_detail = StateLiteSerializer(source="state", read_only=True)
|
state_detail = StateLiteSerializer(source="state", read_only=True)
|
||||||
|
checker_blocks_count = serializers.SerializerMethodField()
|
||||||
|
checker_items_count = serializers.SerializerMethodField()
|
||||||
|
checker_items_completed_count = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Issue
|
model = Issue
|
||||||
|
|
@ -105,6 +109,9 @@ class ExternalContourIssueSerializer(BaseSerializer):
|
||||||
"label_details",
|
"label_details",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"assignee_details",
|
"assignee_details",
|
||||||
|
"checker_blocks_count",
|
||||||
|
"checker_items_count",
|
||||||
|
"checker_items_completed_count",
|
||||||
"state_detail",
|
"state_detail",
|
||||||
"project_detail",
|
"project_detail",
|
||||||
"created_by_detail",
|
"created_by_detail",
|
||||||
|
|
@ -126,6 +133,15 @@ class ExternalContourIssueSerializer(BaseSerializer):
|
||||||
for label_bridge in obj.label_issue.all()
|
for label_bridge in obj.label_issue.all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_checker_blocks_count(self, obj):
|
||||||
|
return build_issue_checker_progress(obj.detail_layout).get("checker_blocks_count", 0)
|
||||||
|
|
||||||
|
def get_checker_items_count(self, obj):
|
||||||
|
return build_issue_checker_progress(obj.detail_layout).get("checker_items_count", 0)
|
||||||
|
|
||||||
|
def get_checker_items_completed_count(self, obj):
|
||||||
|
return build_issue_checker_progress(obj.detail_layout).get("checker_items_completed_count", 0)
|
||||||
|
|
||||||
|
|
||||||
class ExternalContourMirroredAttachmentSerializer(BaseSerializer):
|
class ExternalContourMirroredAttachmentSerializer(BaseSerializer):
|
||||||
uploaded_by = serializers.SerializerMethodField()
|
uploaded_by = serializers.SerializerMethodField()
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ from plane.utils.content_validator import (
|
||||||
validate_binary_data,
|
validate_binary_data,
|
||||||
)
|
)
|
||||||
from plane.utils.date_utils import set_default_issue_start_date
|
from plane.utils.date_utils import set_default_issue_start_date
|
||||||
|
from plane.utils.issue_checker_progress import build_issue_checker_progress
|
||||||
|
|
||||||
|
|
||||||
class IssueFlatSerializer(BaseSerializer):
|
class IssueFlatSerializer(BaseSerializer):
|
||||||
|
|
@ -773,6 +774,9 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||||
|
|
||||||
# Count items
|
# Count items
|
||||||
sub_issues_count = serializers.IntegerField(read_only=True)
|
sub_issues_count = serializers.IntegerField(read_only=True)
|
||||||
|
checker_blocks_count = serializers.SerializerMethodField()
|
||||||
|
checker_items_count = serializers.SerializerMethodField()
|
||||||
|
checker_items_completed_count = serializers.SerializerMethodField()
|
||||||
attachment_count = serializers.IntegerField(read_only=True)
|
attachment_count = serializers.IntegerField(read_only=True)
|
||||||
link_count = serializers.IntegerField(read_only=True)
|
link_count = serializers.IntegerField(read_only=True)
|
||||||
|
|
||||||
|
|
@ -796,6 +800,9 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||||
"label_ids",
|
"label_ids",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"sub_issues_count",
|
"sub_issues_count",
|
||||||
|
"checker_blocks_count",
|
||||||
|
"checker_items_count",
|
||||||
|
"checker_items_completed_count",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
|
@ -809,6 +816,15 @@ class IssueSerializer(DynamicBaseSerializer):
|
||||||
]
|
]
|
||||||
read_only_fields = fields
|
read_only_fields = fields
|
||||||
|
|
||||||
|
def get_checker_blocks_count(self, obj):
|
||||||
|
return build_issue_checker_progress(obj.detail_layout).get("checker_blocks_count", 0)
|
||||||
|
|
||||||
|
def get_checker_items_count(self, obj):
|
||||||
|
return build_issue_checker_progress(obj.detail_layout).get("checker_items_count", 0)
|
||||||
|
|
||||||
|
def get_checker_items_completed_count(self, obj):
|
||||||
|
return build_issue_checker_progress(obj.detail_layout).get("checker_items_completed_count", 0)
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
if (
|
if (
|
||||||
data.get("state_id")
|
data.get("state_id")
|
||||||
|
|
@ -868,6 +884,7 @@ class IssueListDetailSerializer(serializers.Serializer):
|
||||||
"label_ids": self.get_label_ids(instance),
|
"label_ids": self.get_label_ids(instance),
|
||||||
"assignee_ids": self.get_assignee_ids(instance),
|
"assignee_ids": self.get_assignee_ids(instance),
|
||||||
"sub_issues_count": instance.sub_issues_count,
|
"sub_issues_count": instance.sub_issues_count,
|
||||||
|
**build_issue_checker_progress(instance.detail_layout),
|
||||||
"attachment_count": instance.attachment_count,
|
"attachment_count": instance.attachment_count,
|
||||||
"link_count": instance.link_count,
|
"link_count": instance.link_count,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ from plane.utils.grouper import (
|
||||||
)
|
)
|
||||||
from plane.utils.host import base_host
|
from plane.utils.host import base_host
|
||||||
from plane.utils.issue_filters import issue_filters
|
from plane.utils.issue_filters import issue_filters
|
||||||
|
from plane.utils.issue_checker_progress import attach_issue_checker_progress
|
||||||
from plane.utils.order_queryset import order_issue_queryset
|
from plane.utils.order_queryset import order_issue_queryset
|
||||||
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
from plane.utils.paginator import GroupedOffsetPaginator, SubGroupedOffsetPaginator
|
||||||
from plane.utils.timezone_converter import user_timezone_converter
|
from plane.utils.timezone_converter import user_timezone_converter
|
||||||
|
|
@ -190,6 +191,7 @@ class IssueListEndpoint(BaseAPIView):
|
||||||
"label_ids",
|
"label_ids",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"sub_issues_count",
|
"sub_issues_count",
|
||||||
|
"detail_layout",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
|
@ -205,6 +207,7 @@ class IssueListEndpoint(BaseAPIView):
|
||||||
)
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone)
|
issues = user_timezone_converter(issues, datetime_fields, request.user.user_timezone)
|
||||||
|
issues = attach_issue_checker_progress(issues)
|
||||||
return Response(issues, status=status.HTTP_200_OK)
|
return Response(issues, status=status.HTTP_200_OK)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -470,6 +473,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
"label_ids",
|
"label_ids",
|
||||||
"assignee_ids",
|
"assignee_ids",
|
||||||
"sub_issues_count",
|
"sub_issues_count",
|
||||||
|
"detail_layout",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
|
@ -486,6 +490,7 @@ class IssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
issue = user_timezone_converter(issue, datetime_fields, request.user.user_timezone)
|
issue = user_timezone_converter(issue, datetime_fields, request.user.user_timezone)
|
||||||
|
issue = attach_issue_checker_progress([issue])[0] if issue else issue
|
||||||
# Send the model activity
|
# Send the model activity
|
||||||
model_activity.delay(
|
model_activity.delay(
|
||||||
model_name="issue",
|
model_name="issue",
|
||||||
|
|
@ -893,6 +898,7 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||||
# converting the datetime fields in paginated data
|
# converting the datetime fields in paginated data
|
||||||
datetime_fields = ["created_at", "updated_at"]
|
datetime_fields = ["created_at", "updated_at"]
|
||||||
paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
|
paginated_data = user_timezone_converter(paginated_data, datetime_fields, timezone)
|
||||||
|
paginated_data = attach_issue_checker_progress(paginated_data)
|
||||||
|
|
||||||
return paginated_data
|
return paginated_data
|
||||||
|
|
||||||
|
|
@ -930,6 +936,7 @@ class IssuePaginatedViewSet(BaseViewSet):
|
||||||
"link_count",
|
"link_count",
|
||||||
"attachment_count",
|
"attachment_count",
|
||||||
"sub_issues_count",
|
"sub_issues_count",
|
||||||
|
"detail_layout",
|
||||||
]
|
]
|
||||||
|
|
||||||
if str(is_description_required).lower() == "true":
|
if str(is_description_required).lower() == "true":
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from plane.db.models import (
|
||||||
ModuleIssue,
|
ModuleIssue,
|
||||||
IssueLabel,
|
IssueLabel,
|
||||||
)
|
)
|
||||||
|
from plane.utils.issue_checker_progress import attach_issue_checker_progress
|
||||||
from typing import Optional, Dict, Tuple, Any, Union, List
|
from typing import Optional, Dict, Tuple, Any, Union, List
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -118,6 +119,7 @@ def issue_on_results(
|
||||||
"parent_id",
|
"parent_id",
|
||||||
"cycle_id",
|
"cycle_id",
|
||||||
"sub_issues_count",
|
"sub_issues_count",
|
||||||
|
"detail_layout",
|
||||||
"created_at",
|
"created_at",
|
||||||
"updated_at",
|
"updated_at",
|
||||||
"created_by",
|
"created_by",
|
||||||
|
|
@ -141,7 +143,7 @@ def issue_on_results(
|
||||||
original_list.append(sub_group_by)
|
original_list.append(sub_group_by)
|
||||||
|
|
||||||
required_fields.extend(original_list)
|
required_fields.extend(original_list)
|
||||||
return list(issues.values(*required_fields))
|
return attach_issue_checker_progress(issues.values(*required_fields))
|
||||||
|
|
||||||
|
|
||||||
def issue_group_values(
|
def issue_group_values(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# See the LICENSE file for details.
|
||||||
|
|
||||||
|
STRUCTURED_BLOCKS_KEY = "nodedc_structured_blocks"
|
||||||
|
|
||||||
|
|
||||||
|
def build_issue_checker_progress(detail_layout):
|
||||||
|
blocks = detail_layout.get(STRUCTURED_BLOCKS_KEY) if isinstance(detail_layout, dict) else []
|
||||||
|
|
||||||
|
checker_blocks_count = 0
|
||||||
|
checker_items_count = 0
|
||||||
|
checker_items_completed_count = 0
|
||||||
|
|
||||||
|
if not isinstance(blocks, list):
|
||||||
|
blocks = []
|
||||||
|
|
||||||
|
for block in blocks:
|
||||||
|
if not isinstance(block, dict) or block.get("type") != "checker":
|
||||||
|
continue
|
||||||
|
|
||||||
|
checker_blocks_count += 1
|
||||||
|
items = block.get("items")
|
||||||
|
|
||||||
|
if not isinstance(items, list):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
checker_items_count += 1
|
||||||
|
if item.get("checked"):
|
||||||
|
checker_items_completed_count += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
"checker_blocks_count": checker_blocks_count,
|
||||||
|
"checker_items_count": checker_items_count,
|
||||||
|
"checker_items_completed_count": checker_items_completed_count,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def attach_issue_checker_progress(items):
|
||||||
|
prepared_items = list(items)
|
||||||
|
|
||||||
|
for item in prepared_items:
|
||||||
|
detail_layout = item.pop("detail_layout", None)
|
||||||
|
item.update(build_issue_checker_progress(detail_layout))
|
||||||
|
|
||||||
|
return prepared_items
|
||||||
|
|
@ -28,6 +28,7 @@ import { useProjectState } from "@/hooks/store/use-project-state";
|
||||||
import { useUserPermissions } from "@/hooks/store/user";
|
import { useUserPermissions } from "@/hooks/store/user";
|
||||||
import { IssueArchiveService } from "@/services/issue/issue_archive.service";
|
import { IssueArchiveService } from "@/services/issue/issue_archive.service";
|
||||||
import { IssueService } from "@/services/issue/issue.service";
|
import { IssueService } from "@/services/issue/issue.service";
|
||||||
|
import { NodedcWorkItemProgress } from "@/components/issues/issue-layouts/shared/nodedc-work-item-card";
|
||||||
import { ExternalContourDeleteModal } from "./delete-modal";
|
import { ExternalContourDeleteModal } from "./delete-modal";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -143,6 +144,10 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white"
|
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white"
|
||||||
);
|
);
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
const checkerBlocksTotal = issue.checker_blocks_count ?? 0;
|
||||||
|
const checkerItemsTotal = issue.checker_items_count ?? 0;
|
||||||
|
const checkerItemsCompleted = issue.checker_items_completed_count ?? 0;
|
||||||
|
const hasCheckerProgress = checkerBlocksTotal > 0;
|
||||||
const canArchive = canEditTargetIssue && !!selectedState && ARCHIVABLE_STATE_GROUPS.includes(selectedState.group);
|
const canArchive = canEditTargetIssue && !!selectedState && ARCHIVABLE_STATE_GROUPS.includes(selectedState.group);
|
||||||
|
|
||||||
if (!issue) return null;
|
if (!issue) return null;
|
||||||
|
|
@ -459,8 +464,26 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-1 items-center justify-start px-1 pt-7 pb-4 text-left">
|
<div
|
||||||
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
className={cn(
|
||||||
|
"flex flex-1 items-center pb-4",
|
||||||
|
hasCheckerProgress ? "justify-center px-0.5 pt-4 text-left" : "justify-start px-1 pt-7 text-left"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{hasCheckerProgress ? (
|
||||||
|
<div className="flex w-full flex-col items-stretch gap-3">
|
||||||
|
<NodedcWorkItemProgress
|
||||||
|
completedCount={checkerItemsCompleted}
|
||||||
|
totalCount={checkerItemsTotal}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
<div className="line-clamp-4 w-full text-left text-[15px] leading-5 font-medium">
|
||||||
|
{issue.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
<div className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,11 @@ import { useMember } from "@/hooks/store/use-member";
|
||||||
import { useProject } from "@/hooks/store/use-project";
|
import { useProject } from "@/hooks/store/use-project";
|
||||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||||
import { NodedcWorkItemCard, getNodedcWorkItemCardAppearance } from "../shared/nodedc-work-item-card";
|
import {
|
||||||
|
NodedcWorkItemCard,
|
||||||
|
NodedcWorkItemProgress,
|
||||||
|
getNodedcWorkItemCardAppearance,
|
||||||
|
} from "../shared/nodedc-work-item-card";
|
||||||
import type { TRenderQuickActions } from "../list/list-view-types";
|
import type { TRenderQuickActions } from "../list/list-view-types";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -78,6 +82,10 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
const creatorId = creatorDetails?.id || issue.created_by;
|
const creatorId = creatorDetails?.id || issue.created_by;
|
||||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||||
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
const dueDateLabel = issue.target_date ? renderFormattedDate(issue.target_date, "d MMM, yyyy") : t("common.none");
|
||||||
|
const checkerBlocksTotal = issue.checker_blocks_count ?? 0;
|
||||||
|
const checkerItemsTotal = issue.checker_items_count ?? 0;
|
||||||
|
const checkerItemsCompleted = issue.checker_items_completed_count ?? 0;
|
||||||
|
const hasCheckerProgress = checkerBlocksTotal > 0;
|
||||||
const cornerControlClasses = cn(
|
const cornerControlClasses = cn(
|
||||||
"flex h-12 w-12 -translate-x-0.5 -translate-y-0.5 items-center justify-center rounded-full border bg-transparent shadow-none ring-0 transition-colors outline-none",
|
"flex h-12 w-12 -translate-x-0.5 -translate-y-0.5 items-center justify-center rounded-full border bg-transparent shadow-none ring-0 transition-colors outline-none",
|
||||||
isActive
|
isActive
|
||||||
|
|
@ -216,7 +224,16 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const title = (
|
const title = hasCheckerProgress ? (
|
||||||
|
<div className="flex w-full flex-col items-stretch gap-3">
|
||||||
|
<NodedcWorkItemProgress
|
||||||
|
completedCount={checkerItemsCompleted}
|
||||||
|
totalCount={checkerItemsTotal}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
<div className="line-clamp-4 w-full text-left text-[15px] leading-5 font-medium">{issue.name}</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -256,7 +273,9 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||||
contentClassName="min-h-[220px] px-1"
|
contentClassName="min-h-[220px] px-1"
|
||||||
header={header}
|
header={header}
|
||||||
title={title}
|
title={title}
|
||||||
titleContainerClassName="justify-start px-1 pt-7 pb-4 text-left"
|
titleContainerClassName={cn(
|
||||||
|
hasCheckerProgress ? "justify-center px-0.5 pt-4 pb-4 text-left" : "justify-start px-1 pt-7 pb-4 text-left"
|
||||||
|
)}
|
||||||
titleClassName="line-clamp-none w-full text-[15px] leading-5 font-medium"
|
titleClassName="line-clamp-none w-full text-[15px] leading-5 font-medium"
|
||||||
footer={footer}
|
footer={footer}
|
||||||
footerClassName="pointer-events-auto items-end gap-3"
|
footerClassName="pointer-events-auto items-end gap-3"
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,52 @@ export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({
|
||||||
iconBubbleClasses: isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white",
|
iconBubbleClasses: isActive ? "bg-black text-[rgb(var(--nodedc-card-active-rgb))]" : "bg-[#111214] text-white",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type TNodedcWorkItemProgressProps = {
|
||||||
|
completedCount?: number | null;
|
||||||
|
totalCount?: number | null;
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NodedcWorkItemProgress = (props: TNodedcWorkItemProgressProps) => {
|
||||||
|
const { completedCount, totalCount, isActive } = props;
|
||||||
|
const total = Math.max(totalCount ?? 0, 0);
|
||||||
|
const completed = Math.min(Math.max(completedCount ?? 0, 0), total);
|
||||||
|
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||||
|
const segments = [0, 1, 2].map((segmentIndex) => {
|
||||||
|
const segmentStart = segmentIndex * (100 / 3);
|
||||||
|
const segmentEnd = (segmentIndex + 1) * (100 / 3);
|
||||||
|
const filled = Math.min(Math.max(((percentage - segmentStart) / (segmentEnd - segmentStart)) * 100, 0), 100);
|
||||||
|
|
||||||
|
return filled;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full pt-5">
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"absolute -top-1 right-6 translate-x-1/2 text-center text-[8px] leading-[10px] font-medium",
|
||||||
|
isActive ? "text-[#2F4721]" : "text-[#B3B3B8]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{percentage}%
|
||||||
|
</span>
|
||||||
|
<div className="flex h-[2.5px] w-full gap-[2.5px]">
|
||||||
|
{segments.map((segmentFill, segmentIndex) => (
|
||||||
|
<div
|
||||||
|
key={segmentIndex}
|
||||||
|
className={cn("h-full flex-1 overflow-hidden rounded-full", isActive ? "bg-black/20" : "bg-white/16")}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn("h-full rounded-full", isActive ? "bg-black" : "bg-[#C3FF66]")}
|
||||||
|
style={{ width: `${segmentFill}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const NodedcWorkItemCard = ({
|
export const NodedcWorkItemCard = ({
|
||||||
isActive,
|
isActive,
|
||||||
priority,
|
priority,
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,9 @@ export type TBaseIssue = {
|
||||||
estimate_point: string | null;
|
estimate_point: string | null;
|
||||||
|
|
||||||
sub_issues_count: number;
|
sub_issues_count: number;
|
||||||
|
checker_blocks_count?: number | null;
|
||||||
|
checker_items_count?: number | null;
|
||||||
|
checker_items_completed_count?: number | null;
|
||||||
attachment_count: number;
|
attachment_count: number;
|
||||||
link_count: number;
|
link_count: number;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue