diff --git a/apps/desktop/src/renderer/lib/clickPolicy/index.ts b/apps/desktop/src/renderer/lib/clickPolicy/index.ts index e93d92451b2..8506623e04a 100644 --- a/apps/desktop/src/renderer/lib/clickPolicy/index.ts +++ b/apps/desktop/src/renderer/lib/clickPolicy/index.ts @@ -8,12 +8,19 @@ export { LinkHoverHint } from "./components/LinkHoverHint"; export { ShadowClickHint } from "./components/ShadowClickHint"; export { buildHint, UNBOUND_HINT } from "./hint"; export { modifierLabel } from "./modifierLabel"; +export { + buildChangesSidebarFileHint, + type ChangesSidebarFileIntent, + resolveChangesSidebarFileIntent, + tierForChangesSidebarFileIntent, +} from "./policies/changesSidebarFilePolicy"; export { type FolderIntent, folderIntentFor, folderIntentLabel, } from "./policies/folderPolicy"; export type { ClickPolicy } from "./policies/policy"; +export { useChangesSidebarFilePolicy } from "./policies/useChangesSidebarFilePolicy"; export { useInlineFilePolicy } from "./policies/useInlineFilePolicy"; export { useInlineUrlPolicy } from "./policies/useInlineUrlPolicy"; export { useSidebarFilePolicy } from "./policies/useSidebarFilePolicy"; @@ -29,4 +36,5 @@ export type { Surface, TierMode, } from "./types"; +export { usePierreChangesSidebarRowClickPolicy } from "./usePierreChangesSidebarRowClickPolicy"; export { usePierreRowClickPolicy } from "./usePierreRowClickPolicy"; diff --git a/apps/desktop/src/renderer/lib/clickPolicy/policies/changesSidebarFilePolicy.test.ts b/apps/desktop/src/renderer/lib/clickPolicy/policies/changesSidebarFilePolicy.test.ts new file mode 100644 index 00000000000..383a7b0cc0b --- /dev/null +++ b/apps/desktop/src/renderer/lib/clickPolicy/policies/changesSidebarFilePolicy.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "bun:test"; +import type { LinkTierMap } from "../types"; +import { + resolveChangesSidebarFileIntent, + tierForChangesSidebarFileIntent, +} from "./changesSidebarFilePolicy"; + +const map: LinkTierMap = { + plain: "pane", + shift: "newTab", + meta: "pane", + metaShift: "external", +}; + +describe("changes sidebar file policy", () => { + it("keeps plain click on the diff", () => { + expect( + resolveChangesSidebarFileIntent(map, { + metaKey: false, + ctrlKey: false, + shiftKey: false, + }), + ).toBe("diff"); + }); + + it("maps shift-click through the settings map", () => { + expect( + resolveChangesSidebarFileIntent(map, { + metaKey: false, + ctrlKey: false, + shiftKey: true, + }), + ).toBe("diffNewTab"); + }); + + it("maps cmd/ctrl-click to the file when the settings action is pane", () => { + expect( + resolveChangesSidebarFileIntent(map, { + metaKey: true, + ctrlKey: false, + shiftKey: false, + }), + ).toBe("file"); + expect( + resolveChangesSidebarFileIntent(map, { + metaKey: false, + ctrlKey: true, + shiftKey: false, + }), + ).toBe("file"); + }); + + it("maps cmd/ctrl-shift-click to the external editor", () => { + expect( + resolveChangesSidebarFileIntent(map, { + metaKey: true, + ctrlKey: false, + shiftKey: true, + }), + ).toBe("external"); + expect( + resolveChangesSidebarFileIntent(map, { + metaKey: false, + ctrlKey: true, + shiftKey: true, + }), + ).toBe("external"); + }); + + it("returns null for unbound tiers", () => { + expect( + resolveChangesSidebarFileIntent( + { ...map, meta: null }, + { + metaKey: true, + ctrlKey: false, + shiftKey: false, + }, + ), + ).toBeNull(); + }); + + it("finds shortcuts for menu items from the same map", () => { + expect(tierForChangesSidebarFileIntent(map, "diffNewTab")).toBe("shift"); + expect(tierForChangesSidebarFileIntent(map, "file")).toBe("meta"); + expect(tierForChangesSidebarFileIntent(map, "external")).toBe("metaShift"); + }); +}); diff --git a/apps/desktop/src/renderer/lib/clickPolicy/policies/changesSidebarFilePolicy.ts b/apps/desktop/src/renderer/lib/clickPolicy/policies/changesSidebarFilePolicy.ts new file mode 100644 index 00000000000..d8bb9f24627 --- /dev/null +++ b/apps/desktop/src/renderer/lib/clickPolicy/policies/changesSidebarFilePolicy.ts @@ -0,0 +1,59 @@ +import { modifierLabel } from "../modifierLabel"; +import { tierFor } from "../tiers"; +import type { + LinkAction, + LinkTier, + LinkTierMap, + ModifierEvent, +} from "../types"; + +export type ChangesSidebarFileIntent = + | "diff" + | "diffNewTab" + | "file" + | "external"; + +const MODIFIER_TIERS: LinkTier[] = ["shift", "meta", "metaShift"]; + +function intentFor( + tier: LinkTier, + action: LinkAction | null, +): ChangesSidebarFileIntent | null { + if (action === null) return null; + if (action === "external") return "external"; + if (action === "newTab") return "diffNewTab"; + return tier === "plain" ? "diff" : "file"; +} + +function shortIntentLabel(intent: ChangesSidebarFileIntent): string { + if (intent === "diff") return "diff"; + if (intent === "diffNewTab") return "diff in new tab"; + if (intent === "file") return "open file"; + return "editor"; +} + +export function resolveChangesSidebarFileIntent( + map: LinkTierMap, + event: ModifierEvent, +): ChangesSidebarFileIntent | null { + const tier = tierFor(event, "4-tier"); + return intentFor(tier, map[tier]); +} + +export function tierForChangesSidebarFileIntent( + map: LinkTierMap, + intent: ChangesSidebarFileIntent, +): LinkTier | null { + for (const tier of MODIFIER_TIERS) { + if (intentFor(tier, map[tier]) === intent) return tier; + } + return null; +} + +export function buildChangesSidebarFileHint(map: LinkTierMap): string { + return MODIFIER_TIERS.flatMap((tier) => { + const intent = intentFor(tier, map[tier]); + if (intent === null) return []; + return `${modifierLabel(tier)}: ${shortIntentLabel(intent)}`; + }).join(" · "); +} diff --git a/apps/desktop/src/renderer/lib/clickPolicy/policies/useChangesSidebarFilePolicy.ts b/apps/desktop/src/renderer/lib/clickPolicy/policies/useChangesSidebarFilePolicy.ts new file mode 100644 index 00000000000..7b29eebaf61 --- /dev/null +++ b/apps/desktop/src/renderer/lib/clickPolicy/policies/useChangesSidebarFilePolicy.ts @@ -0,0 +1,29 @@ +import { useCallback, useMemo } from "react"; +import { + buildChangesSidebarFileHint, + type ChangesSidebarFileIntent, + resolveChangesSidebarFileIntent, + tierForChangesSidebarFileIntent, +} from "./changesSidebarFilePolicy"; +import { useSidebarFilePolicy } from "./useSidebarFilePolicy"; + +export function useChangesSidebarFilePolicy() { + const policy = useSidebarFilePolicy(); + + const getIntent = useCallback( + (event: Parameters[1]) => + resolveChangesSidebarFileIntent(policy.map, event), + [policy.map], + ); + const tierForIntent = useCallback( + (intent: ChangesSidebarFileIntent) => + tierForChangesSidebarFileIntent(policy.map, intent), + [policy.map], + ); + const hint = useMemo( + () => buildChangesSidebarFileHint(policy.map), + [policy.map], + ); + + return { ...policy, getIntent, tierForIntent, hint }; +} diff --git a/apps/desktop/src/renderer/lib/clickPolicy/usePierreChangesSidebarRowClickPolicy.ts b/apps/desktop/src/renderer/lib/clickPolicy/usePierreChangesSidebarRowClickPolicy.ts new file mode 100644 index 00000000000..0077359f881 --- /dev/null +++ b/apps/desktop/src/renderer/lib/clickPolicy/usePierreChangesSidebarRowClickPolicy.ts @@ -0,0 +1,73 @@ +import { useCallback } from "react"; +import type { ChangesSidebarFileIntent } from "./policies/changesSidebarFilePolicy"; +import { folderIntentFor } from "./policies/folderPolicy"; +import type { ModifierEvent } from "./types"; + +interface UsePierreChangesSidebarRowClickPolicyOptions { + getFileIntent: (event: ModifierEvent) => ChangesSidebarFileIntent | null; + onSelectDiff: (relativePath: string, openInNewTab?: boolean) => void; + onOpenFile: (relativePath: string, openInNewTab?: boolean) => void; + openInExternalEditor: (relativePath: string) => void; +} + +interface UsePierreChangesSidebarRowClickPolicyResult { + onClickCapture: (e: React.MouseEvent) => void; + findFileRow: (e: React.MouseEvent) => HTMLElement | null; +} + +export function usePierreChangesSidebarRowClickPolicy({ + getFileIntent, + onSelectDiff, + onOpenFile, + openInExternalEditor, +}: UsePierreChangesSidebarRowClickPolicyOptions): UsePierreChangesSidebarRowClickPolicyResult { + const findRow = useCallback((e: React.MouseEvent): HTMLElement | null => { + const path = e.nativeEvent.composedPath(); + for (const node of path) { + if (!(node instanceof HTMLElement)) continue; + if (node.getAttribute("data-item-path")) return node; + } + return null; + }, []); + + const findFileRow = useCallback( + (e: React.MouseEvent): HTMLElement | null => { + const row = findRow(e); + const itemPath = row?.getAttribute("data-item-path"); + if (!row || !itemPath || itemPath.endsWith("/")) return null; + return row; + }, + [findRow], + ); + + const onClickCapture = useCallback( + (e: React.MouseEvent) => { + const treePath = findRow(e)?.getAttribute("data-item-path"); + if (!treePath) return; + const trimmed = treePath.endsWith("/") ? treePath.slice(0, -1) : treePath; + + if (treePath.endsWith("/")) { + const intent = folderIntentFor(e); + if (intent === null) return; + e.preventDefault(); + e.stopPropagation(); + if (intent === "external") openInExternalEditor(trimmed); + return; + } + + const intent = getFileIntent(e); + if (intent === null) return; + + e.preventDefault(); + e.stopPropagation(); + + if (intent === "external") openInExternalEditor(trimmed); + else if (intent === "file") onOpenFile(trimmed, false); + else if (intent === "diffNewTab") onSelectDiff(trimmed, true); + else if (intent === "diff") onSelectDiff(trimmed, false); + }, + [findRow, getFileIntent, onSelectDiff, onOpenFile, openInExternalEditor], + ); + + return { onClickCapture, findFileRow }; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/ChangesTreeView.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/ChangesTreeView.tsx index 3fa716f7ebb..ec0e37cdceb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/ChangesTreeView.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/ChangesTreeView.tsx @@ -16,8 +16,8 @@ import { Undo2 } from "lucide-react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ShadowClickHint, - usePierreRowClickPolicy, - useSidebarFilePolicy, + useChangesSidebarFilePolicy, + usePierreChangesSidebarRowClickPolicy, } from "renderer/lib/clickPolicy"; import { useFallthroughIcons } from "renderer/lib/fileIcons"; import { @@ -29,7 +29,10 @@ import { import { DiscardConfirmDialog } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/DiscardConfirmDialog"; import { PierreRowContextMenu } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PierreRowContextMenu"; import type { ChangesetFile } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useChangeset"; -import { toRelativeWorkspacePath } from "shared/absolute-paths"; +import { + toAbsoluteWorkspacePath, + toRelativeWorkspacePath, +} from "shared/absolute-paths"; import type { FoldSignal } from "../../ChangesFileList"; import { FileRowContextMenuItems } from "./components/FileRowContextMenuItems"; import { FolderContextMenuItems } from "./components/FolderContextMenuItems"; @@ -75,7 +78,7 @@ interface ChangesTreeViewProps { * - `renderContextMenu`: file-row actions matching `FileRow`; folder-row * actions (open in editor, copy path) * - hover actions overlay (Discard on unstaged + more-actions ⌄ dropdown) - * - `usePierreRowClickPolicy` for settings-driven click routing + * - `useChangesSidebarFilePolicy` for settings-driven click routing * - selection echo: when the diff pane's file is in this section, focus it * * The discard confirm dialog lives here, not in the per-row menus: Pierre @@ -212,15 +215,21 @@ export const ChangesTreeView = memo(function ChangesTreeView({ return text ? { text } : null; }; - const filePolicy = useSidebarFilePolicy(); - const { onClickCapture, findFileRow } = usePierreRowClickPolicy({ - filePolicy, - onSelectFile: (rel, openInNewTab) => { - lastUserSelectRef.current = rel; - onSelectFile?.(rel, openInNewTab); + const filePolicy = useChangesSidebarFilePolicy(); + const { onClickCapture, findFileRow } = usePierreChangesSidebarRowClickPolicy( + { + getFileIntent: filePolicy.getIntent, + onSelectDiff: (rel, openInNewTab) => { + lastUserSelectRef.current = rel; + onSelectFile?.(rel, openInNewTab); + }, + onOpenFile: (rel, openInNewTab) => { + if (!worktreePath) return; + onOpenFile?.(toAbsoluteWorkspacePath(worktreePath, rel), openInNewTab); + }, + openInExternalEditor: (rel) => onOpenInEditor?.(rel), }, - openInExternalEditor: (rel) => onOpenInEditor?.(rel), - }); + ); // Hoisted so the dialog outlives the menu/hover overlay that triggers it. const [discardTarget, setDiscardTarget] = useState( diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/components/FileRowContextMenuItems/FileRowContextMenuItems.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/components/FileRowContextMenuItems/FileRowContextMenuItems.tsx index 52c8f8ab62f..745124eb49e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/components/FileRowContextMenuItems/FileRowContextMenuItems.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/ChangesTreeView/components/FileRowContextMenuItems/FileRowContextMenuItems.tsx @@ -11,7 +11,10 @@ import { Trash2, Undo2, } from "lucide-react"; -import { modifierLabel, useSidebarFilePolicy } from "renderer/lib/clickPolicy"; +import { + modifierLabel, + useChangesSidebarFilePolicy, +} from "renderer/lib/clickPolicy"; import { PathActionsMenuItems } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/PathActionsMenuItems"; import type { ChangesetFile } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useChangeset"; import { toAbsoluteWorkspacePath } from "shared/absolute-paths"; @@ -52,9 +55,10 @@ export function FileRowContextMenuItems({ const canDiscard = sectionKind === "unstaged"; const isDeleteAction = file.status === "untracked" || file.status === "added"; - const policy = useSidebarFilePolicy(); - const newTabTier = policy.tierForAction("newTab"); - const externalTier = policy.tierForAction("external"); + const policy = useChangesSidebarFilePolicy(); + const diffNewTabTier = policy.tierForIntent("diffNewTab"); + const fileTier = policy.tierForIntent("file"); + const externalTier = policy.tierForIntent("external"); return ( <> @@ -65,9 +69,9 @@ export function FileRowContextMenuItems({ onSelectFile?.(file.path, true)}> Open Diff in New Tab - {newTabTier && ( + {diffNewTabTier && ( - {modifierLabel(newTabTier)} + {modifierLabel(diffNewTabTier)} )} @@ -77,6 +81,9 @@ export function FileRowContextMenuItems({ > Open File + {fileTier && ( + {modifierLabel(fileTier)} + )} absolutePath && onOpenFile?.(absolutePath, true)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx index d35092c147b..e3c0358828a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx @@ -26,7 +26,10 @@ import { Undo2, } from "lucide-react"; import { memo, useState } from "react"; -import { modifierLabel, useSidebarFilePolicy } from "renderer/lib/clickPolicy"; +import { + modifierLabel, + useChangesSidebarFilePolicy, +} from "renderer/lib/clickPolicy"; import { FileIcon } from "renderer/lib/fileIcons"; import { DiscardConfirmDialog } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/DiscardConfirmDialog"; import { StatusIndicator } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/StatusIndicator"; @@ -90,9 +93,10 @@ export const FileRow = memo(function FileRow({ discardMutation.mutate({ workspaceId, filePath: file.path }); }; - const policy = useSidebarFilePolicy(); - const newTabTier = policy.tierForAction("newTab"); - const externalTier = policy.tierForAction("external"); + const policy = useChangesSidebarFilePolicy(); + const diffNewTabTier = policy.tierForIntent("diffNewTab"); + const fileTier = policy.tierForIntent("file"); + const externalTier = policy.tierForIntent("external"); const rowButton = (
@@ -100,10 +104,12 @@ export const FileRow = memo(function FileRow({ type="button" className="flex w-full items-center gap-1.5 py-1 pr-3 pl-3 text-left text-xs hover:bg-accent/50" onClick={(e) => { - const action = policy.getAction(e); - if (action === "external") onOpenInEditor?.(file.path); - else if (action === "newTab") onSelect?.(file.path, true); - else if (action === "pane") onSelect?.(file.path, false); + const intent = policy.getIntent(e); + if (intent === "external") onOpenInEditor?.(file.path); + else if (intent === "file" && absolutePath) + onOpenFile?.(absolutePath, false); + else if (intent === "diffNewTab") onSelect?.(file.path, true); + else if (intent === "diff") onSelect?.(file.path, false); }} > @@ -172,9 +178,9 @@ export const FileRow = memo(function FileRow({ onSelect?.(file.path, true)}> Open Diff in New Tab - {newTabTier && ( + {diffNewTabTier && ( - {modifierLabel(newTabTier)} + {modifierLabel(diffNewTabTier)} )} @@ -184,6 +190,11 @@ export const FileRow = memo(function FileRow({ > Open File + {fileTier && ( + + {modifierLabel(fileTier)} + + )} absolutePath && onOpenFile?.(absolutePath, true)} @@ -226,9 +237,9 @@ export const FileRow = memo(function FileRow({ onSelect?.(file.path, true)}> Open Diff in New Tab - {newTabTier && ( + {diffNewTabTier && ( - {modifierLabel(newTabTier)} + {modifierLabel(diffNewTabTier)} )} @@ -238,6 +249,9 @@ export const FileRow = memo(function FileRow({ > Open File + {fileTier && ( + {modifierLabel(fileTier)} + )} absolutePath && onOpenFile?.(absolutePath, true)} diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts index 44e44070d63..37f91d0f298 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.test.ts @@ -70,6 +70,21 @@ describe("healV2UserPreferences", () => { DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks.metaShift, ); }); + + it("migrates the legacy sidebar file link default to the current default", () => { + const healed = healV2UserPreferences({ + sidebarFileLinks: { + plain: "pane", + shift: "newTab", + meta: "external", + metaShift: "external", + }, + }); + + expect(healed.sidebarFileLinks).toEqual( + DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks, + ); + }); }); describe("healWorkspaceLocalState", () => { diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts index 0c6f3cf091f..694a47584ec 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/dashboardSidebarLocal/schema.ts @@ -195,13 +195,40 @@ const DEFAULT_LINK_TIER_MAP: LinkTierMap = { metaShift: "external", }; -const DEFAULT_SIDEBAR_FILE_LINKS: LinkTierMap = { +const LEGACY_SIDEBAR_FILE_LINKS: LinkTierMap = { plain: "pane", shift: "newTab", meta: "external", metaShift: "external", }; +const DEFAULT_SIDEBAR_FILE_LINKS: LinkTierMap = { + plain: "pane", + shift: "newTab", + meta: "pane", + metaShift: "external", +}; + +function isSameLinkTierMap(a: LinkTierMap, b: LinkTierMap): boolean { + return ( + a.plain === b.plain && + a.shift === b.shift && + a.meta === b.meta && + a.metaShift === b.metaShift + ); +} + +function isCompleteLinkTierMap( + value: Partial, +): value is LinkTierMap { + return ( + "plain" in value && + "shift" in value && + "meta" in value && + "metaShift" in value + ); +} + export const v2UserPreferencesSchema = z.object({ id: z.literal("preferences"), fileLinks: linkTierMapSchema.default(DEFAULT_LINK_TIER_MAP), @@ -273,14 +300,23 @@ export function healV2UserPreferences(raw: unknown): V2UserPreferencesRow { const r = ( raw && typeof raw === "object" ? raw : {} ) as Partial; + const sidebarFileLinks = r.sidebarFileLinks + ? { + ...DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks, + ...r.sidebarFileLinks, + } + : DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks; + const shouldMigrateLegacySidebarFileLinks = + r.sidebarFileLinks && + isCompleteLinkTierMap(r.sidebarFileLinks) && + isSameLinkTierMap(r.sidebarFileLinks, LEGACY_SIDEBAR_FILE_LINKS); return { ...DEFAULT_V2_USER_PREFERENCES, ...r, fileLinks: { ...DEFAULT_V2_USER_PREFERENCES.fileLinks, ...r.fileLinks }, urlLinks: { ...DEFAULT_V2_USER_PREFERENCES.urlLinks, ...r.urlLinks }, - sidebarFileLinks: { - ...DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks, - ...r.sidebarFileLinks, - }, + sidebarFileLinks: shouldMigrateLegacySidebarFileLinks + ? DEFAULT_V2_USER_PREFERENCES.sidebarFileLinks + : sidebarFileLinks, }; }