From 29a3261dd62fb8bd06772e9724e272c7b2fab618 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 8 Jul 2026 23:20:51 +0200 Subject: [PATCH] Restore compact Changes diff controls (#1945) * feat(app): restore compact Changes diff controls Keep the folder tree behind a view toggle while preserving the flat file list as the default. Move secondary diff options into the overflow menu so the Changes toolbar stays compact. * fix(app): mirror whitespace diff option copy * fix(app): polish diff controls review follow-up --- packages/app/e2e/diff-row-alignment.spec.ts | 45 +- packages/app/src/git/diff-flat-items.test.ts | 20 + packages/app/src/git/diff-flat-items.ts | 9 + packages/app/src/git/diff-pane.tsx | 514 ++++++++---------- .../use-changes-preferences/storage.test.ts | 12 +- .../hooks/use-changes-preferences/storage.ts | 3 + packages/app/src/i18n/resources/ar.ts | 6 + packages/app/src/i18n/resources/en.ts | 6 + packages/app/src/i18n/resources/es.ts | 6 + packages/app/src/i18n/resources/fr.ts | 6 + packages/app/src/i18n/resources/ja.ts | 6 + packages/app/src/i18n/resources/pt-BR.ts | 6 + packages/app/src/i18n/resources/ru.ts | 6 + packages/app/src/i18n/resources/zh-CN.ts | 6 + 14 files changed, 348 insertions(+), 303 deletions(-) diff --git a/packages/app/e2e/diff-row-alignment.spec.ts b/packages/app/e2e/diff-row-alignment.spec.ts index 4426f9273..60e1e91b4 100644 --- a/packages/app/e2e/diff-row-alignment.spec.ts +++ b/packages/app/e2e/diff-row-alignment.spec.ts @@ -218,6 +218,38 @@ test("changes diff keeps code rows aligned with the gutter", async ({ page }) => }); }); +test("changes diff switches between flat and tree file lists", async ({ page }) => { + const workspace = await createWorkspaceWithMountedTabDiff(); + await useUnwrappedDiffLines(page); + await openWorkspaceChanges(page, workspace); + + await expectFlatFileList(page); + await expect(page.getByTestId("changes-toggle-layout")).toBeVisible(); + await expect(page.getByTestId("changes-layout-unified")).toHaveCount(0); + await expect(page.getByTestId("changes-layout-split")).toHaveCount(0); + + await page.getByTestId("changes-options-menu").click(); + await expect(page.getByTestId("changes-options-menu-content")).toBeVisible(); + await expect(page.getByTestId("changes-toggle-whitespace")).toContainText("Hide whitespace"); + await expect(page.getByTestId("changes-toggle-wrap-lines")).toContainText("Wrap long lines"); + await expect(page.getByTestId("changes-refresh")).toContainText("Refresh"); + await page.getByTestId("changes-toggle-whitespace").click(); + await page.getByTestId("changes-options-menu").click(); + await expect(page.getByTestId("changes-toggle-whitespace")).toContainText("Show whitespace"); + await page.keyboard.press("Escape"); + + await scrollToLowerUnwrappedDiffRows(page); + await page.getByTestId("changes-toggle-view-mode").click(); + await expect(page.getByTestId("diff-folder-src")).toBeVisible(); + await expect(page.getByTestId("diff-file-0")).toBeVisible(); + + await page.getByTestId("diff-folder-src-toggle").click(); + await expect(page.getByTestId("diff-file-0")).toHaveCount(0); + + await page.getByTestId("changes-toggle-view-mode").click(); + await expectFlatFileList(page); +}); + test("changes diff keeps unwrapped gutter and code rows aligned after code size changes", async ({ page, }) => { @@ -266,13 +298,24 @@ async function useUnwrappedDiffLines(page: Page): Promise { ({ preferencesKey }) => { localStorage.setItem( preferencesKey, - JSON.stringify({ layout: "unified", wrapLines: false, hideWhitespace: false }), + JSON.stringify({ + layout: "unified", + viewMode: "flat", + wrapLines: false, + hideWhitespace: false, + }), ); }, { preferencesKey: CHANGES_PREFERENCES_KEY }, ); } +async function expectFlatFileList(page: Page): Promise { + await expect(page.locator('[data-testid^="diff-folder-"]')).toHaveCount(0); + await expect(page.getByTestId("diff-file-0")).toContainText("use-mounted-tab-set.ts"); + await expect(page.getByTestId("diff-file-0")).toContainText("src"); +} + async function expectDiffCodeFontSize(page: Page, fontSize: number): Promise { await expect .poll(async () => { diff --git a/packages/app/src/git/diff-flat-items.test.ts b/packages/app/src/git/diff-flat-items.test.ts index 14c60f5c4..a3df76492 100644 --- a/packages/app/src/git/diff-flat-items.test.ts +++ b/packages/app/src/git/diff-flat-items.test.ts @@ -22,6 +22,7 @@ describe("buildDiffFlatItems", () => { it("emits a body and a sticky index for each expanded file", () => { const { items, stickyHeaderIndices } = buildDiffFlatItems({ files: [createFile("a.ts"), createFile("b.ts")], + viewMode: "tree", collapsedFolders: new Set(), expandedPaths: new Set(["a.ts"]), }); @@ -32,9 +33,23 @@ describe("buildDiffFlatItems", () => { expect(items[0].type).toBe("header"); }); + it("emits the old flat list without folder rows or indentation", () => { + const { items, stickyHeaderIndices } = buildDiffFlatItems({ + files, + viewMode: "flat", + collapsedFolders: new Set(), + expandedPaths: new Set(["src/app/a.ts"]), + }); + + expect(summarize(items)).toEqual(["a.ts", "body:a.ts", "b.ts"]); + expect(stickyHeaderIndices).toEqual([0]); + expect(items.every((item) => item.type !== "folder")).toBe(true); + }); + it("groups files under compressed folder rows, all expanded by default", () => { const { items } = buildDiffFlatItems({ files, + viewMode: "tree", collapsedFolders: new Set(), expandedPaths: new Set(), }); @@ -45,6 +60,7 @@ describe("buildDiffFlatItems", () => { it("collapsing a folder hides its descendants but keeps the row", () => { const { items } = buildDiffFlatItems({ files, + viewMode: "tree", collapsedFolders: new Set(["src/app/nested"]), expandedPaths: new Set(), }); @@ -54,6 +70,7 @@ describe("buildDiffFlatItems", () => { it("collapsing an ancestor hides everything below it", () => { const { items } = buildDiffFlatItems({ files, + viewMode: "tree", collapsedFolders: new Set(["src/app"]), expandedPaths: new Set(), }); @@ -65,6 +82,7 @@ describe("buildDiffFlatItems", () => { // header at index 3, with the body right after. const { items, stickyHeaderIndices } = buildDiffFlatItems({ files, + viewMode: "tree", collapsedFolders: new Set(), expandedPaths: new Set(["src/app/a.ts"]), }); @@ -84,6 +102,7 @@ describe("buildDiffFlatItems", () => { it("maps tree file rows back to their original files array index", () => { const { items } = buildDiffFlatItems({ files, + viewMode: "tree", collapsedFolders: new Set(), expandedPaths: new Set(), }); @@ -96,6 +115,7 @@ describe("buildDiffFlatItems", () => { describe("sumHeightsBefore", () => { const items = buildDiffFlatItems({ files: [createFile("src/a.ts"), createFile("src/b.ts")], + viewMode: "tree", collapsedFolders: new Set(), expandedPaths: new Set(), }).items; // [folder, a.ts, b.ts] diff --git a/packages/app/src/git/diff-flat-items.ts b/packages/app/src/git/diff-flat-items.ts index 2947f017f..cd60dd97c 100644 --- a/packages/app/src/git/diff-flat-items.ts +++ b/packages/app/src/git/diff-flat-items.ts @@ -30,6 +30,7 @@ export interface DiffFlatItemsResult { export interface BuildDiffFlatItemsInput { files: ParsedDiffFile[]; + viewMode: "flat" | "tree"; /** Full uncompressed directory paths that are collapsed (empty = all expanded). */ collapsedFolders: ReadonlySet; /** File paths whose diff body is expanded. */ @@ -52,6 +53,7 @@ export interface BuildDiffFlatItemsInput { */ export function buildDiffFlatItems({ files, + viewMode, collapsedFolders, expandedPaths, tree, @@ -68,6 +70,13 @@ export function buildDiffFlatItems({ } }; + if (viewMode === "flat") { + for (const [fileIndex, file] of files.entries()) { + pushFile(file, fileIndex, 0); + } + return { items, stickyHeaderIndices }; + } + const indexByPath = new Map(files.map((file, index) => [file.path, index])); const compressedTree = tree ?? compressSingleChildChains(buildDiffTree(files)); const rows = flattenDiffTree(compressedTree, collapsedFolders); diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index 135925193..46a14a96f 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -39,6 +39,7 @@ import { FolderTree, GitCommitHorizontal, GitMerge, + List, ListChevronsDownUp, ListChevronsUpDown, Pilcrow, @@ -1219,7 +1220,6 @@ type PressableStyleFn = ( state: PressableStateCallbackType & { hovered?: boolean; open?: boolean }, ) => StyleProp; -const foregroundIconColorMapping = (theme: Theme) => ({ color: theme.colors.foreground }); const foregroundMutedIconColorMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted }); const ThemedActivityIndicator = withUnistyles(ActivityIndicator); @@ -1230,6 +1230,7 @@ const ThemedWrapText = withUnistyles(WrapText); const ThemedListChevronsDownUp = withUnistyles(ListChevronsDownUp); const ThemedListChevronsUpDown = withUnistyles(ListChevronsUpDown); const ThemedFolderTree = withUnistyles(FolderTree); +const ThemedList = withUnistyles(List); const ThemedGitCommitHorizontal = withUnistyles(GitCommitHorizontal); const ThemedDownload = withUnistyles(Download); const ThemedUpload = withUnistyles(Upload); @@ -1240,101 +1241,89 @@ const ThemedRefreshCcw = withUnistyles(RefreshCcw); const ThemedArchive = withUnistyles(Archive); const ThemedChevronDown = withUnistyles(ChevronDown); -interface DiffLayoutToggleGroupProps { +const DIFF_OPTIONS_WHITESPACE_ICON = ( + +); +const DIFF_OPTIONS_WRAP_ICON = ( + +); + +interface DiffLayoutToggleProps { layout: "unified" | "split"; - unifiedToggleStyle: PressableStyleFn; - splitToggleStyle: PressableStyleFn; - onUnified: () => void; - onSplit: () => void; -} - -function DiffLayoutToggleGroup({ - layout, - unifiedToggleStyle, - splitToggleStyle, - onUnified, - onSplit, -}: DiffLayoutToggleGroupProps) { - const { t } = useTranslation(); - const unifiedLabel = t("workspace.git.diff.unified"); - const splitLabel = t("workspace.git.diff.split"); - return ( - - - - - - - - - {unifiedLabel} - - - - - - - - - - {splitLabel} - - - - ); -} - -interface DiffWhitespaceToggleProps { - hideWhitespace: boolean; isMobile: boolean; toggleStyle: PressableStyleFn; onToggle: () => void; } -function DiffWhitespaceToggle({ - hideWhitespace, - isMobile, - toggleStyle, - onToggle, -}: DiffWhitespaceToggleProps) { +function DiffLayoutToggle({ layout, isMobile, toggleStyle, onToggle }: DiffLayoutToggleProps) { const { t } = useTranslation(); - const label = t("workspace.git.diff.hideWhitespace"); + const label = + layout === "unified" + ? t("workspace.git.diff.switchToSplit") + : t("workspace.git.diff.switchToUnified"); return ( + {layout === "unified" ? ( + + ) : ( + + )} + + + + {label} + + + ); +} + +interface DiffViewModeToggleProps { + viewMode: "flat" | "tree"; + isMobile: boolean; + toggleStyle: PressableStyleFn; + onToggle: () => void; +} + +function DiffViewModeToggle({ + viewMode, + isMobile, + toggleStyle, + onToggle, +}: DiffViewModeToggleProps) { + const { t } = useTranslation(); + const label = + viewMode === "flat" + ? t("workspace.git.diff.showTreeView") + : t("workspace.git.diff.showFlatView"); + return ( + + + - + {viewMode === "flat" ? ( + + ) : ( + + )} @@ -1345,82 +1334,24 @@ function DiffWhitespaceToggle({ } interface DiffFilesToolbarProps { - wrapLines: boolean; - // Controls file-DIFF bodies only (not folders). Named explicitly so the - // folder control below stays unambiguous (Codex item 4). allFileDiffsExpanded: boolean; - showFolderControl: boolean; - allFoldersCollapsed: boolean; isMobile: boolean; - wrapLinesToggleStyle: PressableStyleFn; expandAllToggleStyle: PressableStyleFn; - folderToggleStyle: PressableStyleFn; - onToggleWrapLines: () => void; onToggleExpandAll: () => void; - onToggleCollapseAllFolders: () => void; } function DiffFilesToolbar({ - wrapLines, allFileDiffsExpanded, - showFolderControl, - allFoldersCollapsed, isMobile, - wrapLinesToggleStyle, expandAllToggleStyle, - folderToggleStyle, - onToggleWrapLines, onToggleExpandAll, - onToggleCollapseAllFolders, }: DiffFilesToolbarProps) { const { t } = useTranslation(); - const wrapLinesLabel = wrapLines - ? t("workspace.git.diff.scrollLongLines") - : t("workspace.git.diff.wrapLongLines"); const expandAllLabel = allFileDiffsExpanded ? t("workspace.git.diff.collapseAll") : t("workspace.git.diff.expandAll"); - const folderLabel = allFoldersCollapsed - ? t("workspace.git.diff.expandAllFolders") - : t("workspace.git.diff.collapseAllFolders"); return ( - {showFolderControl ? ( - - - - - - - - {folderLabel} - - - ) : null} - - - - - - - - {wrapLinesLabel} - - void; + overflowToggleStyle: PressableStyleFn; + refreshSupported: boolean; + wrapLines: boolean; + onRefresh: () => void; + onToggleHideWhitespace: () => void; + onToggleWrapLines: () => void; +} + +function DiffOptionsMenu({ + hideWhitespace, + isMobile, + isRefreshing, + overflowToggleStyle, + refreshSupported, + wrapLines, + onRefresh, + onToggleHideWhitespace, + onToggleWrapLines, +}: DiffOptionsMenuProps) { + const { t } = useTranslation(); + const whitespaceLabel = hideWhitespace + ? t("workspace.git.diff.showWhitespace") + : t("workspace.git.diff.hideWhitespace"); + const wrapLinesLabel = wrapLines + ? t("workspace.git.diff.scrollLongLines") + : t("workspace.git.diff.wrapLongLines"); + const optionsLabel = t("workspace.git.diff.options"); + const refreshIcon = useMemo( + () => + isRefreshing ? ( + + ) : ( + + ), + [isRefreshing], + ); + + return ( + + + + + + + + + {optionsLabel} + + + + + {whitespaceLabel} + + + {wrapLinesLabel} + + {refreshSupported ? ( + <> + + + {isRefreshing ? t("workspace.git.diff.refreshing") : t("workspace.git.diff.refresh")} + + + ) : null} + + + ); } const ThemedRotateCw = withUnistyles(RotateCw); const ThemedLoadingSpinner = withUnistyles(LoadingSpinner); -function DiffRefreshButton({ isRefreshing, toggleStyle, onPress }: DiffRefreshButtonProps) { - const { t } = useTranslation(); - const refreshLabel = t("workspace.git.diff.refresh"); - return ( - - - - - {isRefreshing ? ( - - ) : ( - - )} - - - - - {refreshLabel} - - - ); -} - type DiffFlatItemLayoutGetter = NonNullable["getItemLayout"]>; function getUnifiedDiffLineCount(file: ParsedDiffFile): number { @@ -1756,30 +1742,22 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane const { preferences: changesPreferences, updatePreferences: updateChangesPreferences } = useChangesPreferences(); const wrapLines = changesPreferences.wrapLines; + const viewMode = changesPreferences.viewMode; const effectiveLayout = canUseSplitLayout ? changesPreferences.layout : "unified"; const handleToggleWrapLines = useCallback(() => { void updateChangesPreferences({ wrapLines: !wrapLines }); }, [updateChangesPreferences, wrapLines]); - const handleLayoutChange = useCallback( - (nextLayout: "unified" | "split") => { - void updateChangesPreferences({ layout: nextLayout }); - }, - [updateChangesPreferences], - ); - const handleToggleHideWhitespace = useCallback(() => { void updateChangesPreferences({ hideWhitespace: !changesPreferences.hideWhitespace }); }, [changesPreferences.hideWhitespace, updateChangesPreferences]); - const handleLayoutUnified = useCallback(() => { - handleLayoutChange("unified"); - }, [handleLayoutChange]); - - const handleLayoutSplit = useCallback(() => { - handleLayoutChange("split"); - }, [handleLayoutChange]); + const handleToggleLayout = useCallback(() => { + void updateChangesPreferences({ + layout: changesPreferences.layout === "unified" ? "split" : "unified", + }); + }, [changesPreferences.layout, updateChangesPreferences]); const codeFontSize = appSettings.codeFontSize; const diffBodyLineHeight = Math.round(codeFontSize * 1.5); @@ -1796,39 +1774,19 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane }, [appSettings.monoFontFamily, codeFontSize, diffBodyLineHeight]); const diffModeTriggerStyle = useMemo(() => buildDiffModeTriggerStyle(), []); - const unifiedToggleStyle = useMemo( - () => - buildToggleButtonStyle(changesPreferences.layout === "unified", [ - styles.toggleButton, - styles.toggleButtonGroupStart, - ]), - [changesPreferences.layout], + const layoutToggleStyle = useMemo( + () => buildToggleButtonStyle(false, styles.expandAllButton), + [], ); - const splitToggleStyle = useMemo( - () => - buildToggleButtonStyle(changesPreferences.layout === "split", [ - styles.toggleButton, - styles.toggleButtonGroupEnd, - ]), - [changesPreferences.layout], - ); - - const hideWhitespaceToggleStyle = useMemo( - () => buildToggleButtonStyle(changesPreferences.hideWhitespace, styles.expandAllButton), - [changesPreferences.hideWhitespace], - ); - - const wrapLinesToggleStyle = useMemo( - () => buildToggleButtonStyle(wrapLines, styles.expandAllButton), - [wrapLines], + const viewModeToggleStyle = useMemo( + () => buildToggleButtonStyle(viewMode === "tree", styles.expandAllButton), + [viewMode], ); const expandAllToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []); - const folderToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []); - - const refreshToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []); + const overflowToggleStyle = useMemo(() => buildExpandAllButtonStyle(), []); const toast = useToast(); const refreshSupported = useSessionStore( @@ -1995,6 +1953,16 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane [collapsedFoldersArray, allFolderPathSet], ); const diffListRef = useRef>(null); + const handleToggleViewMode = useCallback(() => { + const nextViewMode = viewMode === "flat" ? "tree" : "flat"; + if (nextViewMode === "tree") { + diffListRef.current?.scrollToOffset({ offset: 0, animated: false }); + if (workspaceStateKey) { + setDiffCollapsedFoldersForWorkspace(workspaceStateKey, []); + } + } + void updateChangesPreferences({ viewMode: nextViewMode }); + }, [setDiffCollapsedFoldersForWorkspace, updateChangesPreferences, viewMode, workspaceStateKey]); const scrollbar = useWebScrollViewScrollbar(diffListRef, { enabled: showDesktopWebScrollbar, }); @@ -2012,12 +1980,13 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane const { flatItems, stickyHeaderIndices } = useMemo(() => { const { items, stickyHeaderIndices: stickyIndices } = buildDiffFlatItems({ files, + viewMode, tree: compressedTree, collapsedFolders, expandedPaths, }); return { flatItems: items, stickyHeaderIndices: stickyIndices }; - }, [compressedTree, collapsedFolders, expandedPaths, files]); + }, [compressedTree, collapsedFolders, expandedPaths, files, viewMode]); const getBodyHeightKey = useCallback( (file: ParsedDiffFile): string => { @@ -2248,25 +2217,6 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane } }, [allFileDiffsExpanded, files, setDiffExpandedPathsForWorkspace, workspaceStateKey]); - // Membership, not count: collapsedFolders is already filtered to present folders, so - // "all collapsed" means every present folder path is in it. - const allFoldersCollapsed = - allFolderPaths.length > 0 && allFolderPaths.every((path) => collapsedFolders.has(path)); - - const handleToggleCollapseAllFolders = useCallback(() => { - if (!workspaceStateKey) { - return; - } - // Anchor to the top before collapsing everything so the viewport isn't stranded. - if (!allFoldersCollapsed) { - diffListRef.current?.scrollToOffset({ offset: 0, animated: false }); - } - setDiffCollapsedFoldersForWorkspace( - workspaceStateKey, - allFoldersCollapsed ? [] : allFolderPaths, - ); - }, [allFolderPaths, allFoldersCollapsed, setDiffCollapsedFoldersForWorkspace, workspaceStateKey]); - const renderFlatItem = useCallback( ({ item }: { item: DiffFlatItem }) => { if (item.type === "folder") { @@ -2290,7 +2240,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane file={item.file} isExpanded={item.isExpanded} depth={item.depth} - showDir={false} + showDir={viewMode === "flat"} onToggle={handleToggleExpanded} onHeaderHeightChange={handleHeaderHeightChange} testID={`diff-file-${item.fileIndex}`} @@ -2320,6 +2270,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane handleToggleExpanded, handleToggleFolder, reviewActions, + viewMode, wrapLines, ], ); @@ -2347,6 +2298,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane effectiveLayout, diffBodyTypographyKey, heightVersion, + viewMode, wrapLines, reviewActions, }), @@ -2356,6 +2308,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane effectiveLayout, diffBodyTypographyKey, heightVersion, + viewMode, wrapLines, reviewActions, ], @@ -2485,42 +2438,40 @@ export function GitDiffPane({ serverId, workspaceId, cwd, enabled }: GitDiffPane {canUseSplitLayout ? ( - + ) : null} + {files.length > 0 ? ( + ) : null} - {files.length > 0 ? ( 0} - allFoldersCollapsed={allFoldersCollapsed} isMobile={isMobile} - wrapLinesToggleStyle={wrapLinesToggleStyle} expandAllToggleStyle={expandAllToggleStyle} - folderToggleStyle={folderToggleStyle} - onToggleWrapLines={handleToggleWrapLines} onToggleExpandAll={handleToggleExpandAll} - onToggleCollapseAllFolders={handleToggleCollapseAllFolders} - /> - ) : null} - {refreshSupported ? ( - ) : null} + @@ -2602,46 +2553,9 @@ const styles = StyleSheet.create((theme) => ({ gap: theme.spacing[1], flexWrap: "wrap", }, - toggleButtonGroup: { - flexDirection: "row", - alignItems: "center", - }, - toggleButton: { - alignItems: "center", - justifyContent: "center", - minWidth: { - xs: 32, - sm: 32, - md: 24, - }, - height: { - xs: 32, - sm: 32, - md: 24, - }, - paddingHorizontal: { - xs: theme.spacing[2], - sm: theme.spacing[2], - md: theme.spacing[1], - }, - }, - toggleButtonGroupStart: { - borderTopLeftRadius: theme.borderRadius.base, - borderBottomLeftRadius: theme.borderRadius.base, - }, - toggleButtonGroupEnd: { - borderTopRightRadius: theme.borderRadius.base, - borderBottomRightRadius: theme.borderRadius.base, - }, toggleButtonSelected: { backgroundColor: theme.colors.surface2, }, - refreshIcon: { - width: ICON_SIZE.md, - height: ICON_SIZE.md, - alignItems: "center", - justifyContent: "center", - }, expandAllButton: { flexDirection: "row", alignItems: "center", diff --git a/packages/app/src/hooks/use-changes-preferences/storage.test.ts b/packages/app/src/hooks/use-changes-preferences/storage.test.ts index 044cec1a9..8a00b5746 100644 --- a/packages/app/src/hooks/use-changes-preferences/storage.test.ts +++ b/packages/app/src/hooks/use-changes-preferences/storage.test.ts @@ -28,6 +28,7 @@ describe("loadChangesPreferencesFromStorage", () => { expect(result).toEqual({ layout: "unified", + viewMode: "flat", wrapLines: true, hideWhitespace: false, }); @@ -37,6 +38,7 @@ describe("loadChangesPreferencesFromStorage", () => { it("loads persisted layout and whitespace preferences without rewriting storage", async () => { const persisted = JSON.stringify({ layout: "split", + viewMode: "tree", hideWhitespace: true, wrapLines: false, }); @@ -48,6 +50,7 @@ describe("loadChangesPreferencesFromStorage", () => { expect(result).toEqual({ layout: "split", + viewMode: "tree", hideWhitespace: true, wrapLines: false, }); @@ -64,11 +67,16 @@ describe("saveChangesPreferences", () => { await saveChangesPreferences({ queryClient, - updates: { layout: "split", hideWhitespace: true }, + updates: { layout: "split", viewMode: "tree", hideWhitespace: true }, storage, }); - const expected = { ...DEFAULT_CHANGES_PREFERENCES, layout: "split", hideWhitespace: true }; + const expected = { + ...DEFAULT_CHANGES_PREFERENCES, + layout: "split", + viewMode: "tree", + hideWhitespace: true, + }; expect(queryClient.getQueryData(CHANGES_PREFERENCES_QUERY_KEY)).toEqual(expected); expect(storage.entries.get(CHANGES_PREFERENCES_STORAGE_KEY)).toBe(JSON.stringify(expected)); }); diff --git a/packages/app/src/hooks/use-changes-preferences/storage.ts b/packages/app/src/hooks/use-changes-preferences/storage.ts index 0c6f8ecf4..bfcef3f7f 100644 --- a/packages/app/src/hooks/use-changes-preferences/storage.ts +++ b/packages/app/src/hooks/use-changes-preferences/storage.ts @@ -7,18 +7,21 @@ export const CHANGES_PREFERENCES_QUERY_KEY = ["changes-preferences"]; const changesPreferencesSchema = z.object({ layout: z.enum(["unified", "split"]).optional(), + viewMode: z.enum(["flat", "tree"]).optional(), wrapLines: z.boolean().optional(), hideWhitespace: z.boolean().optional(), }); export interface ChangesPreferences { layout: "unified" | "split"; + viewMode: "flat" | "tree"; wrapLines: boolean; hideWhitespace: boolean; } export const DEFAULT_CHANGES_PREFERENCES: ChangesPreferences = { layout: "unified", + viewMode: "flat", wrapLines: false, hideWhitespace: false, }; diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index 7f92d37ba..22e3f5a5b 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -707,7 +707,13 @@ export const ar: TranslationResources = { tooLarge: "الفرق كبير جدًا بحيث لا يمكن عرضه", unified: "الفرق الموحدة", split: "فرق جنبًا إلى جنب", + switchToUnified: "التبديل إلى الفرق الموحد", + switchToSplit: "التبديل إلى الفرق جنبًا إلى جنب", + showTreeView: "إظهار شجرة المجلدات", + showFlatView: "إظهار قائمة الملفات المسطحة", + options: "خيارات الفرق", hideWhitespace: "إخفاء المسافة البيضاء", + showWhitespace: "إظهار المسافة البيضاء", scrollLongLines: "قم بتمرير الخطوط الطويلة", wrapLongLines: "لف الخطوط الطويلة", collapseAll: "طي كافة الملفات", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 7d828db50..6d5e66b02 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -714,7 +714,13 @@ export const en = { tooLarge: "Diff too large to display", unified: "Unified diff", split: "Side-by-side diff", + switchToUnified: "Switch to unified diff", + switchToSplit: "Switch to side-by-side diff", + showTreeView: "Show folder tree", + showFlatView: "Show flat file list", + options: "Diff options", hideWhitespace: "Hide whitespace", + showWhitespace: "Show whitespace", scrollLongLines: "Scroll long lines", wrapLongLines: "Wrap long lines", collapseAll: "Collapse all files", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index ee5865e30..4461abd9b 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -734,7 +734,13 @@ export const es: TranslationResources = { tooLarge: "La diferencia es demasiado grande para mostrarse", unified: "Diferencia unificada", split: "Diferencia de lado a lado", + switchToUnified: "Cambiar a diferencia unificada", + switchToSplit: "Cambiar a diferencia lado a lado", + showTreeView: "Mostrar árbol de carpetas", + showFlatView: "Mostrar lista plana de archivos", + options: "Opciones de diferencia", hideWhitespace: "Ocultar espacios en blanco", + showWhitespace: "Mostrar espacios en blanco", scrollLongLines: "Desplazarse por largas filas", wrapLongLines: "Envolver largas filas", collapseAll: "Contraer todos los archivos", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index e4b910e5e..1bcf57ac2 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -733,7 +733,13 @@ export const fr: TranslationResources = { tooLarge: "Diff trop grand pour être affiché", unified: "Différentiel unifié", split: "Différent côte à côte", + switchToUnified: "Passer au diff unifié", + switchToSplit: "Passer au diff côte à côte", + showTreeView: "Afficher l'arborescence des dossiers", + showFlatView: "Afficher la liste de fichiers à plat", + options: "Options du diff", hideWhitespace: "Masquer les espaces", + showWhitespace: "Afficher les espaces", scrollLongLines: "Faire défiler les longues lignes", wrapLongLines: "Enroulez les longues lignes", collapseAll: "Réduire tous les fichiers", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 8b7833633..33546846a 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -719,7 +719,13 @@ export const ja: TranslationResources = { tooLarge: "差分が大きすぎて表示できません", unified: "ユニファイド差分", split: "左右比較", + switchToUnified: "ユニファイド差分に切り替え", + switchToSplit: "左右比較差分に切り替え", + showTreeView: "フォルダツリーを表示", + showFlatView: "フラットなファイル一覧を表示", + options: "差分オプション", hideWhitespace: "空白を非表示", + showWhitespace: "空白を表示", scrollLongLines: "長い行をスクロール", wrapLongLines: "長い行を折り返す", collapseAll: "すべて折りたたむ", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 269d40793..5ee07a541 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -725,7 +725,13 @@ export const ptBR: TranslationResources = { tooLarge: "Diff grande demais para exibir", unified: "Diff unificado", split: "Diff lado a lado", + switchToUnified: "Alternar para diff unificado", + switchToSplit: "Alternar para diff lado a lado", + showTreeView: "Mostrar árvore de pastas", + showFlatView: "Mostrar lista plana de arquivos", + options: "Opções do diff", hideWhitespace: "Ocultar espaços em branco", + showWhitespace: "Mostrar espaços em branco", scrollLongLines: "Rolar linhas longas", wrapLongLines: "Quebrar linhas longas", collapseAll: "Recolher todos os arquivos", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index 30ed6e31d..16af21aa6 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -726,7 +726,13 @@ export const ru: TranslationResources = { tooLarge: "Разница слишком велика для отображения", unified: "Единый дифференциал", split: "Параллельная разница", + switchToUnified: "Переключить на объединенный diff", + switchToSplit: "Переключить на diff рядом", + showTreeView: "Показать дерево папок", + showFlatView: "Показать плоский список файлов", + options: "Параметры diff", hideWhitespace: "Скрыть пробелы", + showWhitespace: "Показать пробелы", scrollLongLines: "Прокручивать длинные строки", wrapLongLines: "Перенос длинных строк", collapseAll: "Свернуть все файлы", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 1128f66c9..69792250b 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -701,7 +701,13 @@ export const zhCN: TranslationResources = { tooLarge: "Diff 过大,无法显示", unified: "Unified diff", split: "Side-by-side diff", + switchToUnified: "切换到统一差异视图", + switchToSplit: "切换到并排差异视图", + showTreeView: "显示文件夹树", + showFlatView: "显示平铺文件列表", + options: "Diff 选项", hideWhitespace: "隐藏空白差异", + showWhitespace: "显示空白差异", scrollLongLines: "滚动长行", wrapLongLines: "自动换行长行", collapseAll: "折叠所有文件",