mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-15 06:09:35 +02:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -163,6 +163,7 @@ export function IssuesPage() {
|
||||
visibleStatuses={visibleStatuses}
|
||||
hiddenStatuses={hiddenStatuses}
|
||||
onMoveIssue={handleMoveIssue}
|
||||
childProgressMap={childProgressMap}
|
||||
/>
|
||||
) : (
|
||||
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -203,6 +203,7 @@ export function MyIssuesPage() {
|
||||
visibleStatuses={visibleStatuses}
|
||||
hiddenStatuses={hiddenStatuses}
|
||||
onMoveIssue={handleMoveIssue}
|
||||
childProgressMap={childProgressMap}
|
||||
/>
|
||||
) : (
|
||||
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />
|
||||
|
||||
@@ -160,6 +160,7 @@ function ProjectIssuesTab({ projectIssues }: { projectIssues: Issue[] }) {
|
||||
visibleStatuses={visibleStatuses}
|
||||
hiddenStatuses={hiddenStatuses}
|
||||
onMoveIssue={handleMoveIssue}
|
||||
childProgressMap={childProgressMap}
|
||||
/>
|
||||
) : (
|
||||
<ListView issues={issues} visibleStatuses={visibleStatuses} childProgressMap={childProgressMap} />
|
||||
|
||||
Reference in New Issue
Block a user