feat(views): refine sub-issue progress UI and add to board view

- Move progress badge right after issue title (not pushed to far right)
- Increase progress ring size from 11px to 14px for better visibility
- Add sub-issue progress indicator to board card view
- Thread childProgressMap through BoardView → BoardColumn → BoardCard
This commit is contained in:
Jiang Bohan
2026-04-09 16:48:43 +08:00
parent 874520bda3
commit eea3774240
7 changed files with 40 additions and 12 deletions

View File

@@ -15,6 +15,8 @@ import { PriorityPicker, AssigneePicker, DueDatePicker } from "./pickers";
import { PRIORITY_CONFIG } from "@multica/core/issues/config";
import type { CardProperties } from "@multica/core/issues/stores/view-store";
import { useViewStore } from "@multica/core/issues/stores/view-store-context";
import { ProgressRing } from "./progress-ring";
import type { ChildProgress } from "./list-row";
function formatDate(date: string): string {
return new Date(date).toLocaleDateString("en-US", {
@@ -39,9 +41,11 @@ function PickerWrapper({ children }: { children: React.ReactNode }) {
export const BoardCardContent = memo(function BoardCardContent({
issue,
editable = false,
childProgress,
}: {
issue: Issue;
editable?: boolean;
childProgress?: ChildProgress;
}) {
const storeProperties = useViewStore((s) => s.cardProperties);
const priorityCfg = PRIORITY_CONFIG[issue.priority];
@@ -73,6 +77,16 @@ export const BoardCardContent = memo(function BoardCardContent({
{issue.title}
</p>
{/* Sub-issue progress */}
{childProgress && (
<div className="mt-1.5 inline-flex items-center gap-1 rounded-full bg-muted/60 px-1.5 py-0.5">
<ProgressRing done={childProgress.done} total={childProgress.total} size={14} />
<span className="text-[11px] text-muted-foreground tabular-nums font-medium">
{childProgress.done}/{childProgress.total}
</span>
</div>
)}
{/* Description */}
{showDescription && (
<p className="mt-1 text-xs text-muted-foreground line-clamp-1">
@@ -173,7 +187,7 @@ const animateLayoutChanges: AnimateLayoutChanges = (args) => {
return defaultAnimateLayoutChanges(args);
};
export const DraggableBoardCard = memo(function DraggableBoardCard({ issue }: { issue: Issue }) {
export const DraggableBoardCard = memo(function DraggableBoardCard({ issue, childProgress }: { issue: Issue; childProgress?: ChildProgress }) {
const {
attributes,
listeners,
@@ -204,7 +218,7 @@ export const DraggableBoardCard = memo(function DraggableBoardCard({ issue }: {
href={`/issues/${issue.id}`}
className={`group block transition-colors ${isDragging ? "pointer-events-none" : ""}`}
>
<BoardCardContent issue={issue} editable />
<BoardCardContent issue={issue} editable childProgress={childProgress} />
</AppLink>
</div>
);

View File

@@ -18,17 +18,20 @@ import { useModalStore } from "@multica/core/modals";
import { useViewStoreApi } from "@multica/core/issues/stores/view-store-context";
import { StatusIcon } from "./status-icon";
import { DraggableBoardCard } from "./board-card";
import type { ChildProgress } from "./list-row";
export function BoardColumn({
status,
issueIds,
issueMap,
childProgressMap,
totalCount,
footer,
}: {
status: IssueStatus;
issueIds: string[];
issueMap: Map<string, Issue>;
childProgressMap?: Map<string, ChildProgress>;
totalCount?: number;
footer?: ReactNode;
}) {
@@ -102,7 +105,7 @@ export function BoardColumn({
>
<SortableContext items={issueIds} strategy={verticalListSortingStrategy}>
{resolvedIssues.map((issue) => (
<DraggableBoardCard key={issue.id} issue={issue} />
<DraggableBoardCard key={issue.id} issue={issue} childProgress={childProgressMap?.get(issue.id)} />
))}
</SortableContext>
{issueIds.length === 0 && (

View File

@@ -33,6 +33,7 @@ import { StatusIcon } from "./status-icon";
import { BoardColumn } from "./board-column";
import { BoardCardContent } from "./board-card";
import { InfiniteScrollSentinel } from "./infinite-scroll-sentinel";
import type { ChildProgress } from "./list-row";
const COLUMN_IDS = new Set<string>(ALL_STATUSES);
@@ -93,12 +94,15 @@ function findColumn(
return null;
}
const EMPTY_PROGRESS_MAP = new Map<string, ChildProgress>();
export function BoardView({
issues,
allIssues,
visibleStatuses,
hiddenStatuses,
onMoveIssue,
childProgressMap = EMPTY_PROGRESS_MAP,
}: {
issues: Issue[];
allIssues: Issue[];
@@ -109,6 +113,7 @@ export function BoardView({
newStatus: IssueStatus,
newPosition?: number
) => void;
childProgressMap?: Map<string, ChildProgress>;
}) {
const sortBy = useViewStore((s) => s.sortBy);
const sortDirection = useViewStore((s) => s.sortDirection);
@@ -275,6 +280,7 @@ export function BoardView({
status={status}
issueIds={columns[status] ?? []}
issueMap={issueMapRef.current}
childProgressMap={childProgressMap}
totalCount={status === "done" ? doneTotal : undefined}
footer={
status === "done" && hasMore ? (
@@ -295,7 +301,7 @@ export function BoardView({
<DragOverlay dropAnimation={null}>
{activeIssue ? (
<div className="w-[280px] rotate-2 scale-105 cursor-grabbing opacity-90 shadow-lg shadow-black/10">
<BoardCardContent issue={activeIssue} />
<BoardCardContent issue={activeIssue} childProgress={childProgressMap.get(activeIssue.id)} />
</div>
) : null}
</DragOverlay>

View File

@@ -163,6 +163,7 @@ export function IssuesPage() {
visibleStatuses={visibleStatuses}
hiddenStatuses={hiddenStatuses}
onMoveIssue={handleMoveIssue}
childProgressMap={childProgressMap}
/>
) : (
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />

View File

@@ -57,15 +57,17 @@ export const ListRow = memo(function ListRow({
<span className="w-16 shrink-0 text-xs text-muted-foreground">
{issue.identifier}
</span>
<span className="min-w-0 flex-1 truncate">{issue.title}</span>
{childProgress && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-muted/60 px-1.5 py-0.5">
<ProgressRing done={childProgress.done} total={childProgress.total} size={11} />
<span className="text-[11px] text-muted-foreground tabular-nums font-medium">
{childProgress.done}/{childProgress.total}
<span className="flex min-w-0 flex-1 items-center gap-1.5">
<span className="truncate">{issue.title}</span>
{childProgress && (
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-muted/60 px-1.5 py-0.5">
<ProgressRing done={childProgress.done} total={childProgress.total} size={14} />
<span className="text-[11px] text-muted-foreground tabular-nums font-medium">
{childProgress.done}/{childProgress.total}
</span>
</span>
</span>
)}
)}
</span>
{issue.due_date && (
<span className="shrink-0 text-xs text-muted-foreground">
{formatDate(issue.due_date)}

View File

@@ -203,6 +203,7 @@ export function MyIssuesPage() {
visibleStatuses={visibleStatuses}
hiddenStatuses={hiddenStatuses}
onMoveIssue={handleMoveIssue}
childProgressMap={childProgressMap}
/>
) : (
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />

View File

@@ -160,6 +160,7 @@ function ProjectIssuesTab({ projectIssues }: { projectIssues: Issue[] }) {
visibleStatuses={visibleStatuses}
hiddenStatuses={hiddenStatuses}
onMoveIssue={handleMoveIssue}
childProgressMap={childProgressMap}
/>
) : (
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />