feat(FN-3629): refine branch chip visibility in task cards
Refined branch chip visibility logic in TaskCard (suppressing the chip when the current branch is the default) and added test coverage for those suppression cases, with corresponding documentation updates. Also added chat layout regression tests and fixed a composer drag-pan issue on mobile touch de Fusion-Task-Id: FN-3629
This commit is contained in:
@@ -110,7 +110,7 @@ fn task unarchive FN-001
|
||||
|
||||
### Branch metadata semantics
|
||||
|
||||
Task cards on the board surface branch metadata when present so users can scan both the working branch and merge target without opening task details.
|
||||
Task cards on the board only surface branch metadata when it is non-default/user-meaningful: they hide the conventional auto-generated working branch (`fusion/<task-id>` and suffixed variants) and hide the default merge target (`main`), while still showing custom working branches and non-default merge targets.
|
||||
|
||||
The board header search panel now includes two **board-only** branch filters:
|
||||
- **Working branch** filters by `task.branch`
|
||||
|
||||
@@ -209,6 +209,30 @@ function formatElapsedDuration(elapsedMs: number): string {
|
||||
return `${elapsedDays}d`;
|
||||
}
|
||||
|
||||
function normalizeBranchValue(value: string | undefined): string | null {
|
||||
if (!value) return null;
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
function getVisibleTaskCardBranches(task: Task): { branch: string | null; baseBranch: string | null } {
|
||||
const branch = normalizeBranchValue(task.branch);
|
||||
const baseBranch = normalizeBranchValue(task.baseBranch);
|
||||
const defaultBranchPrefix = `fusion/${task.id.toLowerCase()}`;
|
||||
|
||||
const visibleBranch =
|
||||
branch && (branch === defaultBranchPrefix || branch.startsWith(`${defaultBranchPrefix}-`))
|
||||
? null
|
||||
: branch;
|
||||
|
||||
const visibleBaseBranch = baseBranch?.toLowerCase() === "main" ? null : baseBranch;
|
||||
|
||||
return {
|
||||
branch: visibleBranch,
|
||||
baseBranch: visibleBaseBranch ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function formatElapsedDurationDone(elapsedMs: number): string {
|
||||
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return "";
|
||||
if (elapsedMs === 0) return "";
|
||||
@@ -710,7 +734,8 @@ function TaskCardComponent({
|
||||
const canEdit = EDITABLE_COLUMNS.has(task.column) && !isAgentActive && !isPaused && !queued && onUpdateTask;
|
||||
const hasGitHubBadge = Boolean(task.prInfo || task.issueInfo);
|
||||
const isGitHubImportedTask = task.sourceType === "github_import";
|
||||
const hasBranchMetadata = Boolean(task.branch || task.baseBranch);
|
||||
const branchMetadata = useMemo(() => getVisibleTaskCardBranches(task), [task.id, task.branch, task.baseBranch]);
|
||||
const hasBranchMetadata = Boolean(branchMetadata.branch || branchMetadata.baseBranch);
|
||||
const sourceIssueUrl = getIssueUrlFromMetadata(task.sourceMetadata);
|
||||
const isAgentCreated = isAgentCreatedTask(task);
|
||||
const sourceAgentName = getSourceAgentName(task);
|
||||
@@ -1498,16 +1523,16 @@ function TaskCardComponent({
|
||||
</div>
|
||||
{hasBranchMetadata && (
|
||||
<div className="card-branch-row" aria-label="Branch metadata">
|
||||
{task.branch && (
|
||||
<span className="card-branch-chip" title={task.branch}>
|
||||
{branchMetadata.branch && (
|
||||
<span className="card-branch-chip" title={branchMetadata.branch}>
|
||||
<span className="card-branch-label">Branch</span>
|
||||
<span className="card-branch-value">{task.branch}</span>
|
||||
<span className="card-branch-value">{branchMetadata.branch}</span>
|
||||
</span>
|
||||
)}
|
||||
{task.baseBranch && (
|
||||
<span className="card-branch-chip" title={task.baseBranch}>
|
||||
{branchMetadata.baseBranch && (
|
||||
<span className="card-branch-chip" title={branchMetadata.baseBranch}>
|
||||
<span className="card-branch-label">Base</span>
|
||||
<span className="card-branch-value">{task.baseBranch}</span>
|
||||
<span className="card-branch-value">{branchMetadata.baseBranch}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -171,27 +171,34 @@ describe("TaskCard", () => {
|
||||
expect(screen.queryByText("paused by agent")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders branch and base branch metadata when both are present", () => {
|
||||
it("hides default working branch and default base branch metadata", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ branch: "feature/fn-3423-card-branches", baseBranch: "main" })}
|
||||
task={makeTask({ branch: "fusion/fn-001", baseBranch: "main" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const branchRow = container.querySelector(".card-branch-row");
|
||||
expect(branchRow).not.toBeNull();
|
||||
expect(screen.getByText("Branch")).toBeDefined();
|
||||
expect(screen.getByText("feature/fn-3423-card-branches")).toBeDefined();
|
||||
expect(screen.getByText("Base")).toBeDefined();
|
||||
expect(screen.getByText("main")).toBeDefined();
|
||||
expect(container.querySelector(".card-branch-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders only working branch metadata when baseBranch is absent", () => {
|
||||
it("hides auto-generated suffixed default working branches", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ branch: "fusion/fn-001-2", baseBranch: "main" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-branch-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows only custom working branch metadata when base branch is default", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({ branch: "feature/working-only", baseBranch: undefined })}
|
||||
task={makeTask({ branch: "feature/working-only", baseBranch: "main" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
@@ -202,10 +209,10 @@ describe("TaskCard", () => {
|
||||
expect(screen.queryByText("Base")).toBeNull();
|
||||
});
|
||||
|
||||
it("renders only base branch metadata when branch is absent", () => {
|
||||
it("shows only non-default base branch metadata when working branch is default", () => {
|
||||
render(
|
||||
<TaskCard
|
||||
task={makeTask({ branch: undefined, baseBranch: "release/2026-05" })}
|
||||
task={makeTask({ branch: "fusion/fn-001", baseBranch: "release/2026-05" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
@@ -216,19 +223,24 @@ describe("TaskCard", () => {
|
||||
expect(screen.queryByText("Branch")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not render branch metadata row when both branch fields are absent", () => {
|
||||
it("shows both chips when branch and base branch are both non-default", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
task={makeTask({ branch: undefined, baseBranch: undefined })}
|
||||
task={makeTask({ branch: "feature/fn-3423-card-branches", baseBranch: "develop" })}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-branch-row")).toBeNull();
|
||||
const branchRow = container.querySelector(".card-branch-row");
|
||||
expect(branchRow).not.toBeNull();
|
||||
expect(screen.getByText("Branch")).toBeDefined();
|
||||
expect(screen.getByText("feature/fn-3423-card-branches")).toBeDefined();
|
||||
expect(screen.getByText("Base")).toBeDefined();
|
||||
expect(screen.getByText("develop")).toBeDefined();
|
||||
});
|
||||
|
||||
it("keeps long branch names readable via text and title semantics", () => {
|
||||
it("keeps long non-default branch names readable via text and title semantics", () => {
|
||||
const longBranch = "feature/fn-3423-display-very-long-working-branch-name-for-card-metadata";
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
@@ -1420,6 +1432,18 @@ describe("TaskCard memo comparator provenance behavior", () => {
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false when baseBranch changes", () => {
|
||||
const previousTask = makeTask({ branch: "fusion/fn-001", baseBranch: "main" });
|
||||
const nextTask = makeTask({ branch: "fusion/fn-001", baseBranch: "release/2026-05" });
|
||||
|
||||
expect(
|
||||
__test_areTaskCardPropsEqual(
|
||||
{ task: previousTask, onOpenDetail: noop, addToast: noop } as any,
|
||||
{ task: nextTask, onOpenDetail: noop, addToast: noop } as any,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TaskCard mission badge", () => {
|
||||
|
||||
Reference in New Issue
Block a user