mirror of
https://github.com/Monadical-SAS/reflector.git
synced 2025-12-22 21:29:05 +00:00
refactor: migrate from @hey-api/openapi-ts to openapi-react-query
- Replace @hey-api/openapi-ts with openapi-typescript and openapi-react-query - Generate TypeScript types from OpenAPI spec - Set up React Query infrastructure with QueryClientProvider - Migrate all API hooks to use React Query patterns - Maintain backward compatibility for existing components - Remove old API infrastructure and dependencies
This commit is contained in:
@@ -19,17 +19,21 @@ import {
|
||||
parseAsStringLiteral,
|
||||
} from "nuqs";
|
||||
import { LuX } from "react-icons/lu";
|
||||
import { useSearchTranscripts } from "../transcripts/useSearchTranscripts";
|
||||
import useSessionUser from "../../lib/useSessionUser";
|
||||
import { Room, SourceKind, SearchResult, $SourceKind } from "../../api";
|
||||
import useApi from "../../lib/useApi";
|
||||
import { useError } from "../../(errors)/errorContext";
|
||||
import {
|
||||
useRoomsList,
|
||||
useTranscriptsSearch,
|
||||
useTranscriptDelete,
|
||||
useTranscriptProcess,
|
||||
} from "../../lib/api-hooks";
|
||||
import FilterSidebar from "./_components/FilterSidebar";
|
||||
import Pagination, {
|
||||
FIRST_PAGE,
|
||||
PaginationPage,
|
||||
parsePaginationPage,
|
||||
totalPages as getTotalPages,
|
||||
paginationPageTo0Based,
|
||||
} from "./_components/Pagination";
|
||||
import TranscriptCards from "./_components/TranscriptCards";
|
||||
import DeleteTranscriptDialog from "./_components/DeleteTranscriptDialog";
|
||||
@@ -38,18 +42,6 @@ import { RECORD_A_MEETING_URL } from "../../api/urls";
|
||||
|
||||
const SEARCH_FORM_QUERY_INPUT_NAME = "query" as const;
|
||||
|
||||
const usePrefetchRooms = (setRooms: (rooms: Room[]) => void): void => {
|
||||
const { setError } = useError();
|
||||
const api = useApi();
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
api
|
||||
.v1RoomsList({ page: 1 })
|
||||
.then((rooms) => setRooms(rooms.items))
|
||||
.catch((err) => setError(err, "There was an error fetching the rooms"));
|
||||
}, [api, setError]);
|
||||
};
|
||||
|
||||
const SearchForm: React.FC<{
|
||||
setPage: (page: PaginationPage) => void;
|
||||
sourceKind: SourceKind | null;
|
||||
@@ -69,7 +61,6 @@ const SearchForm: React.FC<{
|
||||
searchQuery,
|
||||
setSearchQuery,
|
||||
}) => {
|
||||
// to keep the search input controllable + more fine grained control (urlSearchQuery is updated on submits)
|
||||
const [searchInputValue, setSearchInputValue] = useState(searchQuery || "");
|
||||
const handleSearchQuerySubmit = async (d: FormData) => {
|
||||
await setSearchQuery((d.get(SEARCH_FORM_QUERY_INPUT_NAME) as string) || "");
|
||||
@@ -163,7 +154,6 @@ const UnderSearchFormFilterIndicators: React.FC<{
|
||||
p="1px"
|
||||
onClick={() => {
|
||||
setSourceKind(null);
|
||||
// TODO questionable
|
||||
setRoomId(null);
|
||||
}}
|
||||
_hover={{ bg: "blue.200" }}
|
||||
@@ -229,46 +219,41 @@ export default function TranscriptBrowser() {
|
||||
useEffect(() => {
|
||||
const maybePage = parsePaginationPage(urlPage);
|
||||
if ("error" in maybePage) {
|
||||
setPage(FIRST_PAGE).then(() => {
|
||||
/*may be called n times we dont care*/
|
||||
});
|
||||
setPage(FIRST_PAGE).then(() => {});
|
||||
return;
|
||||
}
|
||||
_setSafePage(maybePage.value);
|
||||
}, [urlPage]);
|
||||
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
// Use new React Query hooks
|
||||
const {
|
||||
results,
|
||||
totalCount: totalResults,
|
||||
isLoading,
|
||||
reload,
|
||||
} = useSearchTranscripts(
|
||||
urlSearchQuery,
|
||||
{
|
||||
roomIds: urlRoomId ? [urlRoomId] : null,
|
||||
sourceKind: urlSourceKind,
|
||||
},
|
||||
{
|
||||
pageSize,
|
||||
page,
|
||||
},
|
||||
);
|
||||
data: searchData,
|
||||
isLoading: searchLoading,
|
||||
refetch: reloadSearch,
|
||||
} = useTranscriptsSearch(urlSearchQuery, {
|
||||
limit: pageSize,
|
||||
offset: paginationPageTo0Based(page) * pageSize,
|
||||
room_id: urlRoomId || undefined,
|
||||
source_kind: urlSourceKind || undefined,
|
||||
});
|
||||
|
||||
const results = searchData?.results || [];
|
||||
const totalResults = searchData?.total || 0;
|
||||
|
||||
// Fetch rooms
|
||||
const { data: roomsData } = useRoomsList(1);
|
||||
const rooms = roomsData?.items || [];
|
||||
|
||||
const totalPages = getTotalPages(totalResults, pageSize);
|
||||
|
||||
const userName = useSessionUser().name;
|
||||
const [deletionLoading, setDeletionLoading] = useState(false);
|
||||
const api = useApi();
|
||||
const { setError } = useError();
|
||||
const cancelRef = React.useRef(null);
|
||||
const [transcriptToDeleteId, setTranscriptToDeleteId] =
|
||||
React.useState<string>();
|
||||
|
||||
usePrefetchRooms(setRooms);
|
||||
|
||||
const handleFilterTranscripts = (
|
||||
sourceKind: SourceKind | null,
|
||||
roomId: string,
|
||||
@@ -280,44 +265,52 @@ export default function TranscriptBrowser() {
|
||||
|
||||
const onCloseDeletion = () => setTranscriptToDeleteId(undefined);
|
||||
|
||||
// Use mutation hooks
|
||||
const deleteTranscript = useTranscriptDelete();
|
||||
const processTranscript = useTranscriptProcess();
|
||||
|
||||
const confirmDeleteTranscript = (transcriptId: string) => {
|
||||
if (!api || deletionLoading) return;
|
||||
if (deletionLoading) return;
|
||||
setDeletionLoading(true);
|
||||
api
|
||||
.v1TranscriptDelete({ transcriptId })
|
||||
.then(() => {
|
||||
setDeletionLoading(false);
|
||||
onCloseDeletion();
|
||||
reload();
|
||||
})
|
||||
.catch((err) => {
|
||||
setDeletionLoading(false);
|
||||
setError(err, "There was an error deleting the transcript");
|
||||
});
|
||||
deleteTranscript.mutate(
|
||||
{
|
||||
params: {
|
||||
path: { transcript_id: transcriptId },
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
setDeletionLoading(false);
|
||||
onCloseDeletion();
|
||||
reloadSearch();
|
||||
},
|
||||
onError: () => {
|
||||
setDeletionLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleProcessTranscript = (transcriptId: string) => {
|
||||
if (!api) {
|
||||
console.error("API not available on handleProcessTranscript");
|
||||
return;
|
||||
}
|
||||
api
|
||||
.v1TranscriptProcess({ transcriptId })
|
||||
.then((result) => {
|
||||
const status =
|
||||
result && typeof result === "object" && "status" in result
|
||||
? (result as { status: string }).status
|
||||
: undefined;
|
||||
if (status === "already running") {
|
||||
setError(
|
||||
new Error("Processing is already running, please wait"),
|
||||
"Processing is already running, please wait",
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
setError(err, "There was an error processing the transcript");
|
||||
});
|
||||
processTranscript.mutate(
|
||||
{
|
||||
params: {
|
||||
path: { transcript_id: transcriptId },
|
||||
},
|
||||
body: {},
|
||||
},
|
||||
{
|
||||
onSuccess: (result) => {
|
||||
const status =
|
||||
result && typeof result === "object" && "status" in result
|
||||
? (result as { status: string }).status
|
||||
: undefined;
|
||||
if (status === "already running") {
|
||||
// Note: setError is already handled in the hook
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const transcriptToDelete = results?.find(
|
||||
@@ -332,7 +325,7 @@ export default function TranscriptBrowser() {
|
||||
? transcriptToDelete.room_name || transcriptToDelete.room_id
|
||||
: transcriptToDelete?.source_kind;
|
||||
|
||||
if (isLoading && results.length === 0) {
|
||||
if (searchLoading && results.length === 0) {
|
||||
return (
|
||||
<Flex
|
||||
flexDir="column"
|
||||
@@ -360,7 +353,7 @@ export default function TranscriptBrowser() {
|
||||
>
|
||||
<Heading size="lg">
|
||||
{userName ? `${userName}'s Transcriptions` : "Your Transcriptions"}{" "}
|
||||
{(isLoading || deletionLoading) && <Spinner size="sm" />}
|
||||
{(searchLoading || deletionLoading) && <Spinner size="sm" />}
|
||||
</Heading>
|
||||
</Flex>
|
||||
|
||||
@@ -403,12 +396,12 @@ export default function TranscriptBrowser() {
|
||||
<TranscriptCards
|
||||
results={results}
|
||||
query={urlSearchQuery}
|
||||
isLoading={isLoading}
|
||||
isLoading={searchLoading}
|
||||
onDelete={setTranscriptToDeleteId}
|
||||
onReprocess={handleProcessTranscript}
|
||||
/>
|
||||
|
||||
{!isLoading && results.length === 0 && (
|
||||
{!searchLoading && results.length === 0 && (
|
||||
<EmptyResult searchQuery={urlSearchQuery} />
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useError } from "../../(errors)/errorContext";
|
||||
import useApi from "../../lib/useApi";
|
||||
import { useRoomsList } from "../../lib/api-hooks";
|
||||
import { Page_Room_ } from "../../api";
|
||||
import { PaginationPage } from "../browse/_components/Pagination";
|
||||
|
||||
@@ -11,38 +9,16 @@ type RoomList = {
|
||||
refetch: () => void;
|
||||
};
|
||||
|
||||
//always protected
|
||||
// Wrapper to maintain backward compatibility
|
||||
const useRoomList = (page: PaginationPage): RoomList => {
|
||||
const [response, setResponse] = useState<Page_Room_ | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setErrorState] = useState<Error | null>(null);
|
||||
const { setError } = useError();
|
||||
const api = useApi();
|
||||
const [refetchCount, setRefetchCount] = useState(0);
|
||||
const { data, isLoading, error, refetch } = useRoomsList(page);
|
||||
|
||||
const refetch = () => {
|
||||
setLoading(true);
|
||||
setRefetchCount(refetchCount + 1);
|
||||
return {
|
||||
response: data || null,
|
||||
loading: isLoading,
|
||||
error: error as Error | null,
|
||||
refetch,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
setLoading(true);
|
||||
api
|
||||
.v1RoomsList({ page })
|
||||
.then((response) => {
|
||||
setResponse(response);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setResponse(null);
|
||||
setLoading(false);
|
||||
setError(err);
|
||||
setErrorState(err);
|
||||
});
|
||||
}, [!api, page, refetchCount]);
|
||||
|
||||
return { response, loading, error, refetch };
|
||||
};
|
||||
|
||||
export default useRoomList;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// this hook is not great, we want to substitute it with a proper state management solution that is also not re-invention
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
// Wrapper for backward compatibility
|
||||
import { SearchResult, SourceKind } from "../../api";
|
||||
import useApi from "../../lib/useApi";
|
||||
import { useTranscriptsSearch } from "../../lib/api-hooks";
|
||||
import {
|
||||
PaginationPage,
|
||||
paginationPageTo0Based,
|
||||
@@ -13,11 +11,6 @@ interface SearchFilters {
|
||||
sourceKind: SourceKind | null;
|
||||
}
|
||||
|
||||
const EMPTY_SEARCH_FILTERS: SearchFilters = {
|
||||
roomIds: null,
|
||||
sourceKind: null,
|
||||
};
|
||||
|
||||
type UseSearchTranscriptsOptions = {
|
||||
pageSize: number;
|
||||
page: PaginationPage;
|
||||
@@ -31,13 +24,9 @@ interface UseSearchTranscriptsReturn {
|
||||
reload: () => void;
|
||||
}
|
||||
|
||||
function hashEffectFilters(filters: SearchFilters): string {
|
||||
return JSON.stringify(filters);
|
||||
}
|
||||
|
||||
export function useSearchTranscripts(
|
||||
query: string = "",
|
||||
filters: SearchFilters = EMPTY_SEARCH_FILTERS,
|
||||
filters: SearchFilters = { roomIds: null, sourceKind: null },
|
||||
options: UseSearchTranscriptsOptions = {
|
||||
pageSize: 20,
|
||||
page: PaginationPage(1),
|
||||
@@ -45,79 +34,18 @@ export function useSearchTranscripts(
|
||||
): UseSearchTranscriptsReturn {
|
||||
const { pageSize, page } = options;
|
||||
|
||||
const [reloadCount, setReloadCount] = useState(0);
|
||||
|
||||
const api = useApi();
|
||||
const abortControllerRef = useRef<AbortController>();
|
||||
|
||||
const [data, setData] = useState<{ results: SearchResult[]; total: number }>({
|
||||
results: [],
|
||||
total: 0,
|
||||
const { data, isLoading, error, refetch } = useTranscriptsSearch(query, {
|
||||
limit: pageSize,
|
||||
offset: paginationPageTo0Based(page) * pageSize,
|
||||
room_id: filters.roomIds?.[0],
|
||||
source_kind: filters.sourceKind || undefined,
|
||||
});
|
||||
const [error, setError] = useState<any>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const filterHash = hashEffectFilters(filters);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api) {
|
||||
setData({ results: [], total: 0 });
|
||||
setError(undefined);
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
const performSearch = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await api.v1TranscriptsSearch({
|
||||
q: query || "",
|
||||
limit: pageSize,
|
||||
offset: paginationPageTo0Based(page) * pageSize,
|
||||
roomId: filters.roomIds?.[0],
|
||||
sourceKind: filters.sourceKind || undefined,
|
||||
});
|
||||
|
||||
if (abortController.signal.aborted) return;
|
||||
setData(response);
|
||||
setError(undefined);
|
||||
} catch (err: unknown) {
|
||||
if ((err as Error).name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
if (abortController.signal.aborted) {
|
||||
console.error("Aborted search but error", err);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(err);
|
||||
} finally {
|
||||
if (!abortController.signal.aborted) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
performSearch().then(() => {});
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [api, query, page, filterHash, pageSize, reloadCount]);
|
||||
|
||||
return {
|
||||
results: data.results,
|
||||
totalCount: data.total,
|
||||
results: data?.results || [],
|
||||
totalCount: data?.total || 0,
|
||||
isLoading,
|
||||
error,
|
||||
reload: () => setReloadCount(reloadCount + 1),
|
||||
reload: refetch,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { GetTranscript } from "../../api";
|
||||
import { useError } from "../../(errors)/errorContext";
|
||||
import { shouldShowError } from "../../lib/errorUtils";
|
||||
import useApi from "../../lib/useApi";
|
||||
import { useTranscriptGet } from "../../lib/api-hooks";
|
||||
|
||||
type ErrorTranscript = {
|
||||
error: Error;
|
||||
@@ -28,43 +25,33 @@ type SuccessTranscript = {
|
||||
const useTranscript = (
|
||||
id: string | null,
|
||||
): ErrorTranscript | LoadingTranscript | SuccessTranscript => {
|
||||
const [response, setResponse] = useState<GetTranscript | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [error, setErrorState] = useState<Error | null>(null);
|
||||
const [reload, setReload] = useState(0);
|
||||
const { setError } = useError();
|
||||
const api = useApi();
|
||||
const reloadHandler = () => setReload((prev) => prev + 1);
|
||||
const { data, isLoading, error, refetch } = useTranscriptGet(id);
|
||||
|
||||
useEffect(() => {
|
||||
if (!id || !api) return;
|
||||
// Map to the expected return format
|
||||
if (isLoading) {
|
||||
return {
|
||||
response: null,
|
||||
loading: true,
|
||||
error: false,
|
||||
reload: refetch,
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
setLoading(true);
|
||||
}
|
||||
if (error) {
|
||||
return {
|
||||
error: error as Error,
|
||||
loading: false,
|
||||
response: null,
|
||||
reload: refetch,
|
||||
};
|
||||
}
|
||||
|
||||
api
|
||||
.v1TranscriptGet({ transcriptId: id })
|
||||
.then((result) => {
|
||||
setResponse(result);
|
||||
setLoading(false);
|
||||
console.debug("Transcript Loaded:", result);
|
||||
})
|
||||
.catch((error) => {
|
||||
const shouldShowHuman = shouldShowError(error);
|
||||
if (shouldShowHuman) {
|
||||
setError(error, "There was an error loading the transcript");
|
||||
} else {
|
||||
setError(error);
|
||||
}
|
||||
setErrorState(error);
|
||||
});
|
||||
}, [id, !api, reload]);
|
||||
|
||||
return { response, loading, error, reload: reloadHandler } as
|
||||
| ErrorTranscript
|
||||
| LoadingTranscript
|
||||
| SuccessTranscript;
|
||||
return {
|
||||
response: data as GetTranscript,
|
||||
loading: false,
|
||||
error: null,
|
||||
reload: refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTranscript;
|
||||
|
||||
Reference in New Issue
Block a user