UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: трехсекционная шкала прогресса чекеров
This commit is contained in:
@@ -28,6 +28,7 @@ import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { IssueArchiveService } from "@/services/issue/issue_archive.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";
|
||||
|
||||
type Props = {
|
||||
@@ -143,6 +144,10 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||
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 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);
|
||||
|
||||
if (!issue) return null;
|
||||
@@ -459,8 +464,26 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center justify-start px-1 pt-7 pb-4 text-left">
|
||||
<div className="line-clamp-5 max-w-full text-[15px] leading-5 font-medium">{issue.name}</div>
|
||||
<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 className="flex items-center justify-between gap-3" onClick={stopCardPropagation}>
|
||||
|
||||
+22
-3
@@ -21,7 +21,11 @@ import { useMember } from "@/hooks/store/use-member";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { useProjectState } from "@/hooks/store/use-project-state";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
@@ -78,6 +82,10 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||
const creatorId = creatorDetails?.id || issue.created_by;
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
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(
|
||||
"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
|
||||
@@ -216,7 +224,16 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||
</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>
|
||||
);
|
||||
|
||||
@@ -256,7 +273,9 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
|
||||
contentClassName="min-h-[220px] px-1"
|
||||
header={header}
|
||||
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"
|
||||
footer={footer}
|
||||
footerClassName="pointer-events-auto items-end gap-3"
|
||||
|
||||
+46
@@ -37,6 +37,52 @@ export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({
|
||||
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 = ({
|
||||
isActive,
|
||||
priority,
|
||||
|
||||
Reference in New Issue
Block a user