"use client"; import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react"; import { PR_PRESETS as BUILTIN_PR_PRESETS, ISSUE_PRESETS as BUILTIN_ISSUE_PRESETS, type PresetOption, } from "./search-bar"; import { fetchUserSettings } from "@/lib/api/domains/settings-api"; import { fetchGitHubWorkspaceSettings, updateGitHubWorkspaceSettings, } from "@/lib/api/domains/github-api"; import { createQueuedUserSettingsSync } from "@/lib/user-settings-sync"; import { hasUserSettingsSyncFailure } from "@/lib/user-settings-sync-failure"; const STORAGE_KEY = "kandev:github-default-queries:v1"; const MIGRATED_KEY = "kandev:github-default-queries:migrated-to-backend:v1"; const WORKSPACE_MIGRATED_KEY_PREFIX = "kandev:github-default-queries:migrated-to-workspace:v1:"; const SYNC_FAILED_KEY = "kandev:github-default-queries:sync-failed:v1"; export type StoredQueryPreset = { value: string; label: string; filter: string; group: "inbox" | "created"; }; type StoredDefaults = { pr: StoredQueryPreset[]; issue: StoredQueryPreset[]; }; export function toStored(presets: PresetOption[]): StoredQueryPreset[] { return presets.map(({ value, label, filter, group }) => ({ value, label, filter, group })); } function readStorage(): StoredDefaults | null { if (typeof window === "undefined") return null; try { const raw = window.localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw) as unknown; if ( typeof parsed !== "object" || parsed === null || !Array.isArray((parsed as StoredDefaults).pr) || !Array.isArray((parsed as StoredDefaults).issue) ) { return null; } return parsed as StoredDefaults; } catch { return null; } } function writeStorage(defaults: StoredDefaults | null) { if (typeof window === "undefined") return; try { if (defaults === null) { window.localStorage.removeItem(STORAGE_KEY); } else { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(defaults)); } } catch { /* ignore quota / access errors */ } } let snapshot: StoredDefaults | null | undefined = undefined; const listeners = new Set<() => void>(); function publish(next: StoredDefaults | null) { snapshot = next; writeStorage(next); listeners.forEach((fn) => fn()); } function readServerDefaults(value: unknown): StoredDefaults | null | undefined { if (value === null) return null; if ( typeof value !== "object" || !Array.isArray((value as StoredDefaults).pr) || !Array.isArray((value as StoredDefaults).issue) ) { return undefined; } return value as StoredDefaults; } async function readLegacyServerDefaults(): Promise { try { const response = await fetchUserSettings({ cache: "no-store" }); const defaults = readServerDefaults(response.settings.github_default_query_presets); return defaults === undefined ? null : defaults; } catch { return undefined; } } const syncServer = createQueuedUserSettingsSync( SYNC_FAILED_KEY, (defaults) => ({ github_default_query_presets: defaults, }), ); let workspaceSyncQueue = Promise.resolve(); function syncWorkspaceDefaultQueryPresets( workspaceId: string, defaults: StoredDefaults | null, ): Promise { workspaceSyncQueue = workspaceSyncQueue .catch(() => undefined) .then(() => updateGitHubWorkspaceSettings({ workspace_id: workspaceId, default_query_presets: defaults, }).then(() => undefined), ); return workspaceSyncQueue; } function snapshotKey(value: StoredDefaults | null): string { return JSON.stringify(value); } function hasMigratedToBackend(): boolean { if (typeof window === "undefined") return false; return window.localStorage.getItem(MIGRATED_KEY) === "1"; } function markMigratedToBackend(): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(MIGRATED_KEY, "1"); } catch { /* ignore storage failures */ } } function hasMigratedToWorkspace(workspaceId: string): boolean { if (typeof window === "undefined") return false; return window.localStorage.getItem(WORKSPACE_MIGRATED_KEY_PREFIX + workspaceId) === "1"; } function markMigratedToWorkspace(workspaceId: string): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(WORKSPACE_MIGRATED_KEY_PREFIX + workspaceId, "1"); } catch { /* ignore storage failures */ } } function subscribe(cb: () => void) { listeners.add(cb); const onStorage = (event: StorageEvent) => { if (event.key !== STORAGE_KEY) return; snapshot = readStorage(); listeners.forEach((fn) => fn()); }; window.addEventListener("storage", onStorage); return () => { listeners.delete(cb); window.removeEventListener("storage", onStorage); }; } function getSnapshot(): StoredDefaults | null { if (snapshot === undefined) snapshot = readStorage(); return snapshot; } function getServerSnapshot(): StoredDefaults | null { return null; } export function __resetSnapshotForTests() { snapshot = undefined; listeners.forEach((fn) => fn()); } function useLegacyDefaultQueryPresetSync(enabled: boolean) { useEffect(() => { if (!enabled) return; let cancelled = false; const initialKey = snapshotKey(getSnapshot()); fetchUserSettings({ cache: "no-store" }) .then((response) => { const serverDefaults = readServerDefaults(response.settings.github_default_query_presets); if (cancelled || serverDefaults === undefined) return; const local = getSnapshot(); if (snapshotKey(local) !== initialKey) return; if (hasUserSettingsSyncFailure(SYNC_FAILED_KEY)) { void syncServer(local); return; } if (serverDefaults === null && local !== null && !hasMigratedToBackend()) { void syncServer(local); markMigratedToBackend(); return; } publish(serverDefaults); markMigratedToBackend(); }) .catch(() => {}); return () => { cancelled = true; }; }, [enabled]); } function useWorkspaceDefaultQueryPresets(workspaceId: string | null) { const [workspaceDefaults, setWorkspaceDefaults] = useState( undefined, ); const writeSeq = useRef(0); useEffect(() => { if (!workspaceId) { setWorkspaceDefaults(undefined); return; } let cancelled = false; const seq = writeSeq.current; setWorkspaceDefaults(undefined); fetchGitHubWorkspaceSettings(workspaceId) .then(async (settings) => { if (cancelled || seq !== writeSeq.current) return; const defaults = readServerDefaults(settings.default_query_presets); let local: StoredDefaults | null | undefined = getSnapshot(); if (local === null) { local = await readLegacyServerDefaults(); } if (cancelled || seq !== writeSeq.current) return; if ( (defaults === null || defaults === undefined) && local !== undefined && local !== null && !hasMigratedToWorkspace(workspaceId) ) { setWorkspaceDefaults(local); void syncWorkspaceDefaultQueryPresets(workspaceId, local) .then(() => markMigratedToWorkspace(workspaceId)) .catch(() => {}); return; } setWorkspaceDefaults(defaults === undefined ? null : defaults); if (!(local === undefined && (defaults === null || defaults === undefined))) { markMigratedToWorkspace(workspaceId); } }) .catch(() => { if (!cancelled) setWorkspaceDefaults(undefined); }); return () => { cancelled = true; }; }, [workspaceId]); const setWorkspaceDefaultsFromLocal = useCallback((next: StoredDefaults | null) => { writeSeq.current += 1; setWorkspaceDefaults(next); }, []); return { workspaceDefaults, setWorkspaceDefaults: setWorkspaceDefaultsFromLocal }; } export function useDefaultQueryPresets(workspaceId: string | null = null) { const stored = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); const { workspaceDefaults, setWorkspaceDefaults } = useWorkspaceDefaultQueryPresets(workspaceId); useLegacyDefaultQueryPresetSync(!workspaceId); const effectiveStored = workspaceId ? workspaceDefaults : stored; const prPresets = useMemo( () => effectiveStored?.pr ?? toStored(BUILTIN_PR_PRESETS), [effectiveStored], ); const issuePresets = useMemo( () => effectiveStored?.issue ?? toStored(BUILTIN_ISSUE_PRESETS), [effectiveStored], ); const save = useCallback( (defaults: StoredDefaults) => { if (workspaceId && workspaceDefaults === undefined) return; if (workspaceId) { setWorkspaceDefaults(defaults); void syncWorkspaceDefaultQueryPresets(workspaceId, defaults) .then(() => markMigratedToWorkspace(workspaceId)) .catch(() => {}); return; } publish(defaults); void syncServer(defaults); markMigratedToBackend(); }, [workspaceId, workspaceDefaults, setWorkspaceDefaults], ); const reset = useCallback(() => { if (workspaceId && workspaceDefaults === undefined) return; if (workspaceId) { setWorkspaceDefaults(null); void syncWorkspaceDefaultQueryPresets(workspaceId, null) .then(() => markMigratedToWorkspace(workspaceId)) .catch(() => {}); return; } publish(null); void syncServer(null); markMigratedToBackend(); }, [workspaceId, workspaceDefaults, setWorkspaceDefaults]); const isCustomized = effectiveStored !== null && effectiveStored !== undefined; return { prPresets, issuePresets, save, reset, isCustomized }; } /** Resolve full PresetOption[] by merging stored presets with icon lookups from builtins. */ export function resolvePresetOptions( stored: StoredQueryPreset[], builtins: PresetOption[], ): PresetOption[] { const iconMap = new Map(builtins.map((b) => [b.value, b.icon])); const defaultIcon = builtins[0]?.icon; return stored.map((s) => ({ ...s, icon: iconMap.get(s.value) ?? defaultIcon, })); }