From c387330ded68f353d0534e330decae36758fea0b Mon Sep 17 00:00:00 2001 From: Konv Suu Date: Wed, 15 Apr 2026 11:04:22 +0800 Subject: [PATCH 1/2] feat: persist selected clone protocol in code explorer --- .../components/repo/code-explorer-toolbar.tsx | 24 +++++++++++++- .../src/lib/repo-clone-protocol-storage.ts | 33 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 apps/dashboard/src/lib/repo-clone-protocol-storage.ts diff --git a/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx b/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx index ce948f7..31d3657 100644 --- a/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx +++ b/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx @@ -33,6 +33,12 @@ import { githubRepoBranchesQueryOptions, } from "#/lib/github.query"; import type { RepoOverview } from "#/lib/github.types"; +import { + type CloneProtocol, + isCloneProtocol, + persistCloneProtocol, + readStoredCloneProtocol, +} from "#/lib/repo-clone-protocol-storage"; export function CodeExplorerToolbar({ repo, @@ -177,12 +183,24 @@ function BranchSelector({ } function CodePopover({ repo }: { repo: RepoOverview }) { + const [selectedProtocol, setSelectedProtocol] = useState(() => + readStoredCloneProtocol(), + ); const [copied, setCopied] = useState(false); const httpsUrl = `https://github.com/${repo.fullName}.git`; const sshUrl = `git@github.com:${repo.fullName}.git`; const cliCommand = `gh repo clone ${repo.fullName}`; const zipUrl = `https://github.com/${repo.fullName}/archive/refs/heads/${repo.defaultBranch}.zip`; + const handleProtocolChange = useCallback((protocol: string) => { + if (!isCloneProtocol(protocol)) { + return; + } + + setSelectedProtocol(protocol); + persistCloneProtocol(protocol); + }, []); + const handleCopy = useCallback((text: string) => { void navigator.clipboard.writeText(text); setCopied(true); @@ -203,7 +221,11 @@ function CodePopover({ repo }: { repo: RepoOverview }) { Clone {repo.fullName} - + HTTPS diff --git a/apps/dashboard/src/lib/repo-clone-protocol-storage.ts b/apps/dashboard/src/lib/repo-clone-protocol-storage.ts new file mode 100644 index 0000000..efa76f8 --- /dev/null +++ b/apps/dashboard/src/lib/repo-clone-protocol-storage.ts @@ -0,0 +1,33 @@ +export type CloneProtocol = "https" | "ssh" | "cli"; + +const CLONE_PROTOCOL_STORAGE_KEY = "diffkit:repo-clone-protocol"; +const DEFAULT_CLONE_PROTOCOL: CloneProtocol = "cli"; + +const VALID_CLONE_PROTOCOLS = { + https: true, + ssh: true, + cli: true, +} satisfies Record; + +export function isCloneProtocol(value: unknown): value is CloneProtocol { + return typeof value === "string" && value in VALID_CLONE_PROTOCOLS; +} + +export function readStoredCloneProtocol(): CloneProtocol { + if (typeof window === "undefined") { + return DEFAULT_CLONE_PROTOCOL; + } + + try { + const stored = window.localStorage.getItem(CLONE_PROTOCOL_STORAGE_KEY); + return isCloneProtocol(stored) ? stored : DEFAULT_CLONE_PROTOCOL; + } catch { + return DEFAULT_CLONE_PROTOCOL; + } +} + +export function persistCloneProtocol(protocol: CloneProtocol): void { + try { + window.localStorage.setItem(CLONE_PROTOCOL_STORAGE_KEY, protocol); + } catch {} +} From 77fe8386391a616026477e3f5978ed4433f21fbc Mon Sep 17 00:00:00 2001 From: Konv Suu Date: Thu, 16 Apr 2026 16:26:00 +0800 Subject: [PATCH 2/2] apply suggestion --- .../components/repo/code-explorer-toolbar.tsx | 25 +++----- .../src/lib/repo-clone-protocol-storage.ts | 39 ++++++++----- .../src/lib/use-local-storage-state.ts | 58 +++++++++++++++++++ 3 files changed, 89 insertions(+), 33 deletions(-) create mode 100644 apps/dashboard/src/lib/use-local-storage-state.ts diff --git a/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx b/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx index 31d3657..16f5fc4 100644 --- a/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx +++ b/apps/dashboard/src/components/repo/code-explorer-toolbar.tsx @@ -33,12 +33,7 @@ import { githubRepoBranchesQueryOptions, } from "#/lib/github.query"; import type { RepoOverview } from "#/lib/github.types"; -import { - type CloneProtocol, - isCloneProtocol, - persistCloneProtocol, - readStoredCloneProtocol, -} from "#/lib/repo-clone-protocol-storage"; +import { useRepoCloneProtocol } from "#/lib/repo-clone-protocol-storage"; export function CodeExplorerToolbar({ repo, @@ -183,23 +178,19 @@ function BranchSelector({ } function CodePopover({ repo }: { repo: RepoOverview }) { - const [selectedProtocol, setSelectedProtocol] = useState(() => - readStoredCloneProtocol(), - ); + const [selectedProtocol, setSelectedProtocol] = useRepoCloneProtocol(); const [copied, setCopied] = useState(false); const httpsUrl = `https://github.com/${repo.fullName}.git`; const sshUrl = `git@github.com:${repo.fullName}.git`; const cliCommand = `gh repo clone ${repo.fullName}`; const zipUrl = `https://github.com/${repo.fullName}/archive/refs/heads/${repo.defaultBranch}.zip`; - const handleProtocolChange = useCallback((protocol: string) => { - if (!isCloneProtocol(protocol)) { - return; - } - - setSelectedProtocol(protocol); - persistCloneProtocol(protocol); - }, []); + const handleProtocolChange = useCallback( + (protocol: string) => { + setSelectedProtocol(protocol); + }, + [setSelectedProtocol], + ); const handleCopy = useCallback((text: string) => { void navigator.clipboard.writeText(text); diff --git a/apps/dashboard/src/lib/repo-clone-protocol-storage.ts b/apps/dashboard/src/lib/repo-clone-protocol-storage.ts index efa76f8..2eb5476 100644 --- a/apps/dashboard/src/lib/repo-clone-protocol-storage.ts +++ b/apps/dashboard/src/lib/repo-clone-protocol-storage.ts @@ -1,3 +1,6 @@ +import { useCallback } from "react"; +import { useLocalStorageState } from "./use-local-storage-state"; + export type CloneProtocol = "https" | "ssh" | "cli"; const CLONE_PROTOCOL_STORAGE_KEY = "diffkit:repo-clone-protocol"; @@ -13,21 +16,25 @@ export function isCloneProtocol(value: unknown): value is CloneProtocol { return typeof value === "string" && value in VALID_CLONE_PROTOCOLS; } -export function readStoredCloneProtocol(): CloneProtocol { - if (typeof window === "undefined") { - return DEFAULT_CLONE_PROTOCOL; - } - - try { - const stored = window.localStorage.getItem(CLONE_PROTOCOL_STORAGE_KEY); - return isCloneProtocol(stored) ? stored : DEFAULT_CLONE_PROTOCOL; - } catch { - return DEFAULT_CLONE_PROTOCOL; - } -} +export function useRepoCloneProtocol() { + const [cloneProtocol, setCloneProtocol] = useLocalStorageState( + CLONE_PROTOCOL_STORAGE_KEY, + { + defaultValue: DEFAULT_CLONE_PROTOCOL, + validate: isCloneProtocol, + }, + ); + + const setStoredCloneProtocol = useCallback( + (protocol: string) => { + if (!isCloneProtocol(protocol)) { + return; + } + + setCloneProtocol(protocol); + }, + [setCloneProtocol], + ); -export function persistCloneProtocol(protocol: CloneProtocol): void { - try { - window.localStorage.setItem(CLONE_PROTOCOL_STORAGE_KEY, protocol); - } catch {} + return [cloneProtocol, setStoredCloneProtocol] as const; } diff --git a/apps/dashboard/src/lib/use-local-storage-state.ts b/apps/dashboard/src/lib/use-local-storage-state.ts new file mode 100644 index 0000000..236a275 --- /dev/null +++ b/apps/dashboard/src/lib/use-local-storage-state.ts @@ -0,0 +1,58 @@ +import { useCallback, useState } from "react"; + +type LocalStorageStateOptions = { + defaultValue: T; + validate: (value: unknown) => value is T; + parse?: (raw: string) => unknown; + serialize?: (value: T) => string; +}; + +export function useLocalStorageState( + key: string, + { + defaultValue, + validate, + parse = (raw) => raw, + serialize = String, + }: LocalStorageStateOptions, +) { + const [value, setValue] = useState(() => { + if (typeof window === "undefined") { + return defaultValue; + } + + try { + const stored = window.localStorage.getItem(key); + if (stored === null) { + return defaultValue; + } + + const parsed = parse(stored); + return validate(parsed) ? parsed : defaultValue; + } catch { + return defaultValue; + } + }); + + const setStoredValue = useCallback( + (nextValue: T | ((currentValue: T) => T)) => { + setValue((currentValue) => { + const resolvedValue = + typeof nextValue === "function" + ? (nextValue as (currentValue: T) => T)(currentValue) + : nextValue; + + try { + window.localStorage.setItem(key, serialize(resolvedValue)); + } catch { + // ignore + } + + return resolvedValue; + }); + }, + [key, serialize], + ); + + return [value, setStoredValue] as const; +}