feat(FN-2921): merge fusion/fn-2921
- feat(FN-2921): complete Step 2 — apply filename-first truncation in change lists - feat(FN-2921): complete Step 1 — prioritize filename-first path truncation Fusion-Task-Id: FN-2921
This commit is contained in:
@@ -469,6 +469,7 @@ describe("Settings view", () => {
|
||||
controller.setInteractiveView("settings");
|
||||
|
||||
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
|
||||
await waitForFrameContains(lastFrame, "──── Remote ────");
|
||||
stdin.write("\t");
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
|
||||
@@ -529,6 +530,7 @@ describe("Settings view", () => {
|
||||
controller.setInteractiveView("settings");
|
||||
|
||||
const { lastFrame, stdin, unmount } = render(renderDashboardAppNode(controller));
|
||||
await waitForFrameContains(lastFrame, "──── Remote ────");
|
||||
stdin.write("\t");
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import type { MergeDetails, Column } from "@fusion/core";
|
||||
import { highlightDiff } from "../utils/highlightDiff";
|
||||
import { truncateMiddle } from "../utils/truncatePath";
|
||||
import "./TaskDiffShared.css";
|
||||
import "./ChangesDiffModal.css";
|
||||
|
||||
@@ -222,7 +223,7 @@ export function ChangesDiffModal({
|
||||
>
|
||||
{getStatusLabel(file.status)}
|
||||
</span>
|
||||
<span className="changes-diff-file-path">{file.path}</span>
|
||||
<span className="changes-diff-file-path">{truncateMiddle(file.path)}</span>
|
||||
<span className="changes-diff-file-stat">
|
||||
+{file.additions} -{file.deletions}
|
||||
</span>
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Task } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import { useConfirm } from "../hooks/useConfirm";
|
||||
import { truncateMiddle } from "../utils/truncatePath";
|
||||
import { useModalResizePersist } from "../hooks/useModalResizePersist";
|
||||
import { useOverlayDismiss } from "../hooks/useOverlayDismiss";
|
||||
import type {
|
||||
@@ -1114,7 +1115,7 @@ function ChangesPanel({
|
||||
/>
|
||||
</label>
|
||||
<FileStatusIcon status={f.status} />
|
||||
<span className="gm-file-name" title={f.file}>{f.file}</span>
|
||||
<span className="gm-file-name" title={f.file}>{truncateMiddle(f.file)}</span>
|
||||
<FileStatusBadge status={f.status} />
|
||||
<button
|
||||
className="gm-icon-btn"
|
||||
@@ -1186,7 +1187,7 @@ function ChangesPanel({
|
||||
/>
|
||||
</label>
|
||||
<FileStatusIcon status={f.status} />
|
||||
<span className="gm-file-name" title={f.file}>{f.file}</span>
|
||||
<span className="gm-file-name" title={f.file}>{truncateMiddle(f.file)}</span>
|
||||
<FileStatusBadge status={f.status} />
|
||||
<button
|
||||
className="gm-icon-btn"
|
||||
|
||||
@@ -2122,7 +2122,6 @@
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 12px;
|
||||
|
||||
@@ -20,13 +20,11 @@ describe("truncateMiddle", () => {
|
||||
expect(truncateMiddle(path, 60)).toBe(path);
|
||||
});
|
||||
|
||||
it("truncates a long path from the middle", () => {
|
||||
it("truncates a long path by prioritizing the filename suffix", () => {
|
||||
const path = "packages/dashboard/app/components/TaskChangesTab.tsx";
|
||||
const result = truncateMiddle(path, 30);
|
||||
expect(result).toContain("...");
|
||||
expect(result).toBe(".../TaskChangesTab.tsx");
|
||||
expect(result.length).toBeLessThanOrEqual(30);
|
||||
// Filename should be preserved
|
||||
expect(result.endsWith("TaskChangesTab.tsx")).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves the full path when under maxLength", () => {
|
||||
@@ -84,12 +82,11 @@ describe("truncateMiddle", () => {
|
||||
expect(result).toContain("...");
|
||||
});
|
||||
|
||||
it("preserves start portion when truncating", () => {
|
||||
it("does not require preserving the start when filename suffix fits", () => {
|
||||
const path = "packages/dashboard/app/components/TaskChangesTab.tsx";
|
||||
const result = truncateMiddle(path, 35);
|
||||
expect(result.startsWith("packages")).toBe(true);
|
||||
expect(result).toContain("...");
|
||||
expect(result.endsWith("TaskChangesTab.tsx")).toBe(true);
|
||||
expect(result).toBe(".../TaskChangesTab.tsx");
|
||||
expect(result.endsWith("/TaskChangesTab.tsx")).toBe(true);
|
||||
});
|
||||
|
||||
it("works with paths that have dots but no slashes", () => {
|
||||
@@ -103,4 +100,18 @@ describe("truncateMiddle", () => {
|
||||
const result = truncateMiddle(path, 60);
|
||||
expect(result.length).toBeLessThanOrEqual(60);
|
||||
});
|
||||
|
||||
it("shows full filename for deeply nested paths when filename fits", () => {
|
||||
const path = "a/b/c/d/e/f/Component.tsx";
|
||||
const result = truncateMiddle(path, 20);
|
||||
expect(result).toBe(".../Component.tsx");
|
||||
expect(result.endsWith("/Component.tsx")).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves extension when filename fits", () => {
|
||||
const path = "deeply/nested/another/path/verylongname.test.tsx";
|
||||
const result = truncateMiddle(path, 24);
|
||||
expect(result).toBe("...verylongname.test.tsx");
|
||||
expect(result.endsWith(".test.tsx")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -24,6 +24,13 @@ export function truncateMiddle(path: string, maxLength: number = 60): string {
|
||||
return path.slice(0, headLen) + ellipsis;
|
||||
}
|
||||
|
||||
const suffixWithSlash = path.slice(lastSep);
|
||||
// Primary behavior: always prefer showing full filename (and extension)
|
||||
// when it can fit with an ellipsis prefix.
|
||||
if (ellipsis.length + suffixWithSlash.length <= maxLength) {
|
||||
return ellipsis + suffixWithSlash;
|
||||
}
|
||||
|
||||
// Find all separator positions to use as candidate split points
|
||||
const seps: number[] = [];
|
||||
for (let i = 1; i < path.length; i++) {
|
||||
@@ -63,12 +70,6 @@ export function truncateMiddle(path: string, maxLength: number = 60): string {
|
||||
|
||||
if (bestResult) return bestResult;
|
||||
|
||||
// Fallback: just ellipsis + the filename portion (with leading "/")
|
||||
const suffixWithSlash = path.slice(lastSep);
|
||||
if (ellipsis.length + suffixWithSlash.length <= maxLength) {
|
||||
return ellipsis + suffixWithSlash;
|
||||
}
|
||||
|
||||
// Filename itself is too long — truncate from the end
|
||||
const endLen = maxLength - ellipsis.length;
|
||||
return ellipsis + path.slice(path.length - endLen);
|
||||
|
||||
Reference in New Issue
Block a user