mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-30 00:05:33 +08:00
refactor(command-palette): consolidate fetch source hooks behind useApiSource
This commit is contained in:
53
src/components/command-palette/sources/useApiSource.ts
Normal file
53
src/components/command-palette/sources/useApiSource.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export type ApiSourceState<T> = {
|
||||||
|
items: T[];
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useApiSource<T, R = unknown>(opts: {
|
||||||
|
enabled: boolean;
|
||||||
|
deps: React.DependencyList;
|
||||||
|
fetcher: (signal: AbortSignal) => Promise<Response>;
|
||||||
|
parse: (raw: R) => T[];
|
||||||
|
}): ApiSourceState<T> {
|
||||||
|
const [items, setItems] = useState<T[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
|
||||||
|
const { enabled, deps, fetcher, parse } = opts;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) {
|
||||||
|
setItems([]);
|
||||||
|
setError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
fetcher(controller.signal)
|
||||||
|
.then((r) => r.json() as Promise<R>)
|
||||||
|
.then((data) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
setItems(parse(data));
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (controller.signal.aborted) return;
|
||||||
|
if (err instanceof DOMException && err.name === 'AbortError') return;
|
||||||
|
setItems([]);
|
||||||
|
setError(err instanceof Error ? err : new Error(String(err)));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!controller.signal.aborted) setIsLoading(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => controller.abort();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [enabled, ...deps]);
|
||||||
|
|
||||||
|
return { items, isLoading, error };
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { authenticatedFetch } from '../../../utils/api';
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
|
|
||||||
|
import { useApiSource } from './useApiSource';
|
||||||
|
|
||||||
export type BranchResult = {
|
export type BranchResult = {
|
||||||
name: string;
|
name: string;
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
@@ -13,35 +13,20 @@ interface BranchesResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useBranchesSource(projectId: string | undefined, enabled: boolean) {
|
export function useBranchesSource(projectId: string | undefined, enabled: boolean) {
|
||||||
const [items, setItems] = useState<BranchResult[]>([]);
|
return useApiSource<BranchResult, BranchesResponse>({
|
||||||
|
enabled: enabled && !!projectId,
|
||||||
useEffect(() => {
|
deps: [projectId],
|
||||||
if (!enabled || !projectId) {
|
fetcher: (signal) => {
|
||||||
setItems([]);
|
const params = new URLSearchParams({ project: projectId! });
|
||||||
return;
|
return authenticatedFetch(`/api/git/branches?${params.toString()}`, { signal });
|
||||||
}
|
},
|
||||||
let cancelled = false;
|
parse: (data) => {
|
||||||
const params = new URLSearchParams({ project: projectId });
|
const list = data.branches ?? [];
|
||||||
authenticatedFetch(`/api/git/branches?${params.toString()}`)
|
return list.map<BranchResult>((b) => ({
|
||||||
.then((r) => r.json() as Promise<BranchesResponse>)
|
name: b.name,
|
||||||
.then((data) => {
|
isCurrent: Boolean(b.current),
|
||||||
if (cancelled) return;
|
isRemote: Boolean(b.isRemote),
|
||||||
const list = data.branches ?? [];
|
}));
|
||||||
setItems(
|
},
|
||||||
list.map<BranchResult>((b) => ({
|
});
|
||||||
name: b.name,
|
|
||||||
isCurrent: Boolean(b.current),
|
|
||||||
isRemote: Boolean(b.isRemote),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setItems([]);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [projectId, enabled]);
|
|
||||||
|
|
||||||
return { items };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { authenticatedFetch } from '../../../utils/api';
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
|
|
||||||
|
import { useApiSource } from './useApiSource';
|
||||||
|
|
||||||
export type CommitResult = {
|
export type CommitResult = {
|
||||||
hash: string;
|
hash: string;
|
||||||
shortHash: string;
|
shortHash: string;
|
||||||
@@ -15,44 +15,21 @@ interface CommitsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useCommitsSource(projectId: string | undefined, enabled: boolean) {
|
export function useCommitsSource(projectId: string | undefined, enabled: boolean) {
|
||||||
const [items, setItems] = useState<CommitResult[]>([]);
|
return useApiSource<CommitResult, CommitsResponse>({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
enabled: enabled && !!projectId,
|
||||||
|
deps: [projectId],
|
||||||
useEffect(() => {
|
fetcher: (signal) => {
|
||||||
if (!enabled || !projectId) {
|
const params = new URLSearchParams({ project: projectId!, limit: '50' });
|
||||||
setItems([]);
|
return authenticatedFetch(`/api/git/commits?${params.toString()}`, { signal });
|
||||||
return;
|
},
|
||||||
}
|
parse: (data) => {
|
||||||
let cancelled = false;
|
if (!data.commits) return [];
|
||||||
setIsLoading(true);
|
return data.commits.map<CommitResult>((c) => ({
|
||||||
const params = new URLSearchParams({ project: projectId, limit: '50' });
|
hash: c.hash,
|
||||||
authenticatedFetch(`/api/git/commits?${params.toString()}`)
|
shortHash: c.hash.slice(0, 7),
|
||||||
.then((r) => r.json() as Promise<CommitsResponse>)
|
message: c.message,
|
||||||
.then((data) => {
|
author: c.author,
|
||||||
if (cancelled) return;
|
}));
|
||||||
if (!data.commits) {
|
},
|
||||||
setItems([]);
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
setItems(
|
|
||||||
data.commits.map<CommitResult>((c) => ({
|
|
||||||
hash: c.hash,
|
|
||||||
shortHash: c.hash.slice(0, 7),
|
|
||||||
message: c.message,
|
|
||||||
author: c.author,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setItems([]);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsLoading(false);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [projectId, enabled]);
|
|
||||||
|
|
||||||
return { items, isLoading };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { api } from '../../../utils/api';
|
import { api } from '../../../utils/api';
|
||||||
|
|
||||||
|
import { useApiSource } from './useApiSource';
|
||||||
|
|
||||||
export type FileResult = {
|
export type FileResult = {
|
||||||
path: string;
|
path: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -28,36 +28,15 @@ function flatten(nodes: FileNode[], out: FileResult[]): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useFilesSource(projectId: string | undefined, enabled: boolean) {
|
export function useFilesSource(projectId: string | undefined, enabled: boolean) {
|
||||||
const [items, setItems] = useState<FileResult[]>([]);
|
return useApiSource<FileResult, unknown>({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
enabled: enabled && !!projectId,
|
||||||
|
deps: [projectId],
|
||||||
useEffect(() => {
|
fetcher: (signal) => api.getFiles(projectId!, { signal }),
|
||||||
if (!enabled || !projectId) {
|
parse: (data) => {
|
||||||
setItems([]);
|
const tree: FileNode[] = Array.isArray(data) ? (data as FileNode[]) : [];
|
||||||
return;
|
const flat: FileResult[] = [];
|
||||||
}
|
flatten(tree, flat);
|
||||||
let cancelled = false;
|
return flat;
|
||||||
setIsLoading(true);
|
},
|
||||||
api
|
});
|
||||||
.getFiles(projectId)
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((data: unknown) => {
|
|
||||||
if (cancelled) return;
|
|
||||||
const tree: FileNode[] = Array.isArray(data) ? (data as FileNode[]) : [];
|
|
||||||
const flat: FileResult[] = [];
|
|
||||||
flatten(tree, flat);
|
|
||||||
setItems(flat);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setItems([]);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsLoading(false);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [projectId, enabled]);
|
|
||||||
|
|
||||||
return { items, isLoading };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { authenticatedFetch } from '../../../utils/api';
|
||||||
|
|
||||||
import { api } from '../../../utils/api';
|
|
||||||
import type { LLMProvider, ProjectSession } from '../../../types/app';
|
import type { LLMProvider, ProjectSession } from '../../../types/app';
|
||||||
|
|
||||||
|
import { useApiSource } from './useApiSource';
|
||||||
|
|
||||||
export type SessionResult = {
|
export type SessionResult = {
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -17,45 +17,28 @@ interface SessionsResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useSessionsSource(projectId: string | undefined, enabled: boolean) {
|
export function useSessionsSource(projectId: string | undefined, enabled: boolean) {
|
||||||
const [items, setItems] = useState<SessionResult[]>([]);
|
return useApiSource<SessionResult, SessionsResponse>({
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
enabled: enabled && !!projectId,
|
||||||
|
deps: [projectId],
|
||||||
useEffect(() => {
|
fetcher: (signal) => {
|
||||||
if (!enabled || !projectId) {
|
const params = new URLSearchParams({ limit: '50', offset: '0' });
|
||||||
setItems([]);
|
return authenticatedFetch(
|
||||||
return;
|
`/api/projects/${encodeURIComponent(projectId!)}/sessions?${params.toString()}`,
|
||||||
}
|
{ signal },
|
||||||
let cancelled = false;
|
);
|
||||||
setIsLoading(true);
|
},
|
||||||
api
|
parse: (data) => {
|
||||||
.projectSessions(projectId, { limit: 50 })
|
const all: ProjectSession[] = [
|
||||||
.then((r) => r.json() as Promise<SessionsResponse>)
|
...(data.sessions ?? []),
|
||||||
.then((data) => {
|
...(data.cursorSessions ?? []),
|
||||||
if (cancelled) return;
|
...(data.codexSessions ?? []),
|
||||||
const all: ProjectSession[] = [
|
...(data.geminiSessions ?? []),
|
||||||
...(data.sessions ?? []),
|
];
|
||||||
...(data.cursorSessions ?? []),
|
return all.map<SessionResult>((s) => ({
|
||||||
...(data.codexSessions ?? []),
|
id: s.id,
|
||||||
...(data.geminiSessions ?? []),
|
label: (s.title || s.summary || s.name || s.id) as string,
|
||||||
];
|
provider: s.__provider,
|
||||||
setItems(
|
}));
|
||||||
all.map<SessionResult>((s) => ({
|
},
|
||||||
id: s.id,
|
});
|
||||||
label: (s.title || s.summary || s.name || s.id) as string,
|
|
||||||
provider: s.__provider,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
if (!cancelled) setItems([]);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
if (!cancelled) setIsLoading(false);
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
cancelled = true;
|
|
||||||
};
|
|
||||||
}, [projectId, enabled]);
|
|
||||||
|
|
||||||
return { items, isLoading };
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user