feat(card): progress narrative and humanised commit history
This commit is contained in:
parent
30446153c8
commit
8711c36833
4 changed files with 135 additions and 0 deletions
|
|
@ -6,6 +6,8 @@ import { AppShell } from "@/components/shell/app-shell";
|
|||
import { TopBar } from "@/components/shell/top-bar";
|
||||
import { CardHeader } from "@/components/card/card-header";
|
||||
import { CardActions } from "@/components/card/card-actions";
|
||||
import { CardNarrative } from "@/components/card/card-narrative";
|
||||
import { CommitHistory } from "@/components/card/commit-history";
|
||||
import { SubtaskList } from "@/components/card/subtask-list";
|
||||
import { CommentThread } from "@/components/card/comment-thread";
|
||||
import { ActivityTimeline } from "@/components/card/activity-timeline";
|
||||
|
|
@ -64,6 +66,9 @@ export default async function CardPage({
|
|||
projectSlug={projectSlug}
|
||||
/>
|
||||
|
||||
<CardNarrative narrative={detail.card.narrative ?? ""} />
|
||||
<CommitHistory commits={detail.commits} />
|
||||
|
||||
<SubtaskList
|
||||
cardId={detail.card.id}
|
||||
subtasks={detail.subtasks}
|
||||
|
|
|
|||
34
src/components/card/card-narrative.tsx
Normal file
34
src/components/card/card-narrative.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* The generated progress update.
|
||||
*
|
||||
* Labelled as written from the commit history — the same honesty the dashboard's
|
||||
* methodology panel applies to the effort figures. A client should never have to
|
||||
* wonder whether a person wrote this.
|
||||
*/
|
||||
export function CardNarrative({ narrative }: { narrative: string }) {
|
||||
if (!narrative.trim()) return null;
|
||||
|
||||
return (
|
||||
<section className="space-y-2 rounded-[var(--radius-tile)] border border-border-subtle bg-surface p-4">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-text-secondary">
|
||||
Progress
|
||||
</h2>
|
||||
|
||||
<div className="space-y-2 text-sm text-text-secondary">
|
||||
{narrative
|
||||
.split(/\n{2,}/)
|
||||
.map((paragraph) => paragraph.trim())
|
||||
.filter(Boolean)
|
||||
.map((paragraph, index) => (
|
||||
<p key={index} className="whitespace-pre-wrap">
|
||||
{paragraph}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-text-muted">
|
||||
Written automatically from the change history for this item.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
83
src/components/card/commit-history.tsx
Normal file
83
src/components/card/commit-history.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Code2 } from "lucide-react";
|
||||
|
||||
import { relativeTime } from "@/lib/time/relative";
|
||||
import type { CommitRecord } from "@/lib/pb/types";
|
||||
|
||||
/**
|
||||
* A card's commits, in the client's language.
|
||||
*
|
||||
* Two rules, both from the spec:
|
||||
* - A commit with no summary shows its raw subject. Never a gap — a card that
|
||||
* renders blank because the provider was down looks like no work happened.
|
||||
* - The raw commit is always one toggle away. The humanised text is a
|
||||
* translation, not a replacement, and a client who wants to check it must be
|
||||
* able to.
|
||||
*/
|
||||
export function CommitHistory({ commits }: { commits: CommitRecord[] }) {
|
||||
const [showRaw, setShowRaw] = useState(false);
|
||||
|
||||
if (commits.length === 0) return null;
|
||||
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-text-secondary">
|
||||
Work done
|
||||
</h2>
|
||||
<span className="tabular text-xs text-text-muted">
|
||||
{commits.length} {commits.length === 1 ? "change" : "changes"}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowRaw((value) => !value)}
|
||||
aria-pressed={showRaw}
|
||||
className="ml-auto inline-flex items-center gap-1.5 text-[11px] text-text-muted hover:text-text-secondary"
|
||||
>
|
||||
<Code2 size={12} strokeWidth={2} />
|
||||
{showRaw ? "Hide technical detail" : "Show technical detail"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ol className="space-y-2.5">
|
||||
{commits.map((commit) => (
|
||||
<li key={commit.id} className="flex gap-2.5">
|
||||
<span
|
||||
aria-hidden
|
||||
className="mt-[7px] h-1.5 w-1.5 shrink-0 rounded-full bg-border-strong"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm text-text-secondary">
|
||||
{/* Fall back to the raw subject — never render nothing. */}
|
||||
{commit.summary || commit.subject}
|
||||
</p>
|
||||
<p className="mt-0.5 text-[11px] text-text-muted">
|
||||
{relativeTime(commit.authored_at)}
|
||||
</p>
|
||||
|
||||
{showRaw && (
|
||||
<p className="mt-1.5 rounded bg-surface px-2 py-1.5 font-mono text-[11px] text-text-muted">
|
||||
{commit.url ? (
|
||||
<a
|
||||
href={commit.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="hover:text-brand"
|
||||
>
|
||||
{commit.short_sha}
|
||||
</a>
|
||||
) : (
|
||||
commit.short_sha
|
||||
)}{" "}
|
||||
{commit.subject}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import type {
|
|||
ClientRecord,
|
||||
ColumnRecord,
|
||||
CommentRecord,
|
||||
CommitRecord,
|
||||
ProjectRecord,
|
||||
SubtaskRecord,
|
||||
UserRecord,
|
||||
|
|
@ -23,6 +24,10 @@ export interface CardDetail {
|
|||
activity: ActivityRecord[];
|
||||
/** Display names by user id, for comments and the timeline. */
|
||||
names: Record<string, string>;
|
||||
/** Non-merge commits linked to this card, oldest first, with AI summaries
|
||||
* when present and the raw subject otherwise. Used by the Progress section
|
||||
* and the humanised commit history on the card page. */
|
||||
commits: CommitRecord[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -83,6 +88,13 @@ export async function loadCardDetail(
|
|||
sort: "-created",
|
||||
});
|
||||
|
||||
// Linked commits, oldest first, in plain English where available. Used by
|
||||
// the Progress section and the humanised commit history on the card page.
|
||||
const commits = await pb.collection("commits").getFullList<CommitRecord>({
|
||||
filter: `card = "${cardId}" && is_merge = false`,
|
||||
sort: "authored_at",
|
||||
});
|
||||
|
||||
const userIds = new Set<string>();
|
||||
for (const c of comments) if (c.author) userIds.add(c.author);
|
||||
for (const a of activity) if (a.actor) userIds.add(a.actor);
|
||||
|
|
@ -107,6 +119,7 @@ export async function loadCardDetail(
|
|||
comments,
|
||||
activity,
|
||||
names,
|
||||
commits,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Reference in a new issue