Files
kandev/apps/web/components/github/my-github/use-default-query-presets.ts
T
Carlos Florêncio b9eb6ae058 refactor(web): remove legacy settings migration (#1704)
* refactor(web): remove legacy settings migration

* fix(web): hydrate portable settings from boot state

* fix(web): preserve sidebar sync contracts

* fix(web): retain portable settings filters

* fix(web): retry sidebar settings writes

* fix(web): retain sidebar boot settings

* fix(websocket): avoid shutdown teardown leaks

* fix(boot): normalize sidebar task preferences

* fix(settings): preserve saved views during hydration

* fix(jira): avoid duplicate saved-view syncs

* fix(jira): preserve saved views on fetch failure

* fix(jira): retry saved-view hydration failures

* refactor(jira): simplify hydration request cleanup

* fix(websocket): close late clients after hub shutdown
2026-07-16 00:06:41 +01:00

203 lines
6.0 KiB
TypeScript

"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";
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 }));
}
let snapshot: StoredDefaults | null | undefined = undefined;
let snapshotVersion = 0;
const listeners = new Set<() => void>();
function publish(next: StoredDefaults | null) {
snapshot = next;
snapshotVersion += 1;
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;
}
const syncServer = createQueuedUserSettingsSync<StoredDefaults | null>((defaults) => ({
github_default_query_presets: defaults,
}));
let workspaceSyncQueue = Promise.resolve();
function syncWorkspaceDefaultQueryPresets(
workspaceId: string,
defaults: StoredDefaults | null,
): Promise<void> {
workspaceSyncQueue = workspaceSyncQueue
.catch(() => undefined)
.then(() =>
updateGitHubWorkspaceSettings({
workspace_id: workspaceId,
default_query_presets: defaults,
}).then(() => undefined),
);
return workspaceSyncQueue;
}
function subscribe(cb: () => void) {
listeners.add(cb);
return () => {
listeners.delete(cb);
};
}
function getSnapshot(): StoredDefaults | null {
return snapshot ?? null;
}
function getServerSnapshot(): StoredDefaults | null {
return null;
}
export function __resetSnapshotForTests() {
snapshot = undefined;
snapshotVersion = 0;
listeners.forEach((fn) => fn());
}
function useUserDefaultQueryPresetSync(enabled: boolean) {
useEffect(() => {
if (!enabled) return;
let cancelled = false;
const initialVersion = snapshotVersion;
fetchUserSettings({ cache: "no-store" })
.then((response) => {
const serverDefaults = readServerDefaults(response.settings.github_default_query_presets);
if (cancelled || serverDefaults === undefined || snapshotVersion !== initialVersion) return;
publish(serverDefaults);
})
.catch(() => {});
return () => {
cancelled = true;
};
}, [enabled]);
}
function useWorkspaceDefaultQueryPresets(workspaceId: string | null) {
const [workspaceDefaults, setWorkspaceDefaults] = useState<StoredDefaults | null | undefined>(
undefined,
);
const writeSeq = useRef(0);
useEffect(() => {
if (!workspaceId) {
setWorkspaceDefaults(undefined);
return;
}
let cancelled = false;
const seq = writeSeq.current;
setWorkspaceDefaults(undefined);
fetchGitHubWorkspaceSettings(workspaceId)
.then((settings) => {
if (cancelled || seq !== writeSeq.current) return;
const defaults = readServerDefaults(settings.default_query_presets);
setWorkspaceDefaults(defaults === undefined ? null : defaults);
})
.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);
useUserDefaultQueryPresetSync(!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).catch(() => {});
return;
}
publish(defaults);
void syncServer(defaults);
},
[workspaceId, workspaceDefaults, setWorkspaceDefaults],
);
const reset = useCallback(() => {
if (workspaceId && workspaceDefaults === undefined) return;
if (workspaceId) {
setWorkspaceDefaults(null);
void syncWorkspaceDefaultQueryPresets(workspaceId, null).catch(() => {});
return;
}
publish(null);
void syncServer(null);
}, [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,
}));
}