Skip to content

Commit b7de238

Browse files
committed
ref useOrganizationRepositoriesWithSettings
1 parent 229bcb4 commit b7de238

File tree

7 files changed

+267
-113
lines changed

7 files changed

+267
-113
lines changed

static/app/components/events/autofix/preferences/hooks/useOrganizationRepositories.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {useCallback, useEffect, useMemo, useRef} from 'react';
22

33
import type {Repository, RepositoryWithSettings} from 'sentry/types/integrations';
4+
import type {Organization} from 'sentry/types/organization';
5+
import {apiOptions} from 'sentry/utils/api/apiOptions';
46
import getApiUrl from 'sentry/utils/api/getApiUrl';
57
import useFetchSequentialPages from 'sentry/utils/api/useFetchSequentialPages';
68
import type {ApiQueryKey} from 'sentry/utils/queryClient';
@@ -10,6 +12,9 @@ interface Props {
1012
query?: Record<string, string>;
1113
}
1214

15+
/**
16+
* @deprecated Use organizationRepositoriesInfiniteOptions instead.
17+
*/
1318
export function useOrganizationRepositories<T extends Repository = Repository>(
1419
{query = {}} = {} as Props
1520
) {
@@ -59,9 +64,21 @@ export function useOrganizationRepositories<T extends Repository = Repository>(
5964
);
6065
}
6166

62-
// TODO(ryan953): express this in typescript instead of having the extra function
63-
export function useOrganizationRepositoriesWithSettings() {
64-
return useOrganizationRepositories<RepositoryWithSettings>({
65-
query: {expand: 'settings'},
66-
});
67+
export function organizationRepositoriesInfiniteOptions({
68+
organization,
69+
query,
70+
staleTime,
71+
}: {
72+
organization: Organization;
73+
query?: {per_page: number};
74+
staleTime?: number;
75+
}) {
76+
return apiOptions.asInfinite<RepositoryWithSettings[]>()(
77+
'/organizations/$organizationIdOrSlug/repos/',
78+
{
79+
path: {organizationIdOrSlug: organization.slug},
80+
query: {expand: 'settings', per_page: 100, ...query},
81+
staleTime: staleTime ?? 0,
82+
}
83+
);
6784
}

static/app/utils/api/apiFetch.tsx

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type {QueryFunctionContext} from '@tanstack/react-query';
22

3-
import type {ApiQueryKey} from 'sentry/utils/queryClient';
3+
import type {ParsedHeader} from 'sentry/utils/parseLinkHeader';
4+
import type {ApiQueryKey, InfiniteApiQueryKey} from 'sentry/utils/queryClient';
45
import {parseQueryKey, QUERY_API_CLIENT} from 'sentry/utils/queryClient';
56

67
export type ApiResponse<TResponseData = unknown> = {
@@ -13,7 +14,7 @@ export type ApiResponse<TResponseData = unknown> = {
1314
};
1415

1516
export default async function apiFetch<TQueryFnData = unknown>(
16-
context: QueryFunctionContext<ApiQueryKey>
17+
context: QueryFunctionContext<ApiQueryKey, never>
1718
): Promise<ApiResponse<TQueryFnData>> {
1819
const {url, options} = parseQueryKey(context.queryKey);
1920

@@ -26,13 +27,42 @@ export default async function apiFetch<TQueryFnData = unknown>(
2627
headers: options?.headers,
2728
});
2829

29-
const xhits = response!.getResponseHeader('X-Hits') ?? null;
30-
const xmaxhits = response!.getResponseHeader('X-Max-Hits') ?? null;
30+
const hits = response!.getResponseHeader('X-Hits') ?? null;
31+
const maxHits = response!.getResponseHeader('X-Max-Hits') ?? null;
3132
return {
3233
headers: {
3334
Link: response!.getResponseHeader('Link') ?? undefined,
34-
'X-Hits': xhits === null ? undefined : Number(xhits),
35-
'X-Max-Hits': xmaxhits === null ? undefined : Number(xmaxhits),
35+
'X-Hits': hits === null ? undefined : Number(hits),
36+
'X-Max-Hits': maxHits === null ? undefined : Number(maxHits),
37+
},
38+
json: json as TQueryFnData,
39+
};
40+
}
41+
42+
export async function apiFetchInfinite<TQueryFnData = unknown>(
43+
context: QueryFunctionContext<InfiniteApiQueryKey, null | undefined | ParsedHeader>
44+
): Promise<ApiResponse<TQueryFnData>> {
45+
const {url, options} = parseQueryKey(context.queryKey);
46+
47+
const [json, , response] = await QUERY_API_CLIENT.requestPromise(url, {
48+
includeAllArgs: true,
49+
host: options?.host,
50+
method: options?.method ?? 'GET',
51+
data: options?.data,
52+
query: {
53+
...options?.query,
54+
cursor: context.pageParam?.cursor ?? options?.query?.cursor,
55+
},
56+
headers: options?.headers,
57+
});
58+
59+
const hits = response!.getResponseHeader('X-Hits') ?? null;
60+
const maxHits = response!.getResponseHeader('X-Max-Hits') ?? null;
61+
return {
62+
headers: {
63+
Link: response!.getResponseHeader('Link') ?? undefined,
64+
'X-Hits': hits === null ? undefined : Number(hits),
65+
'X-Max-Hits': maxHits === null ? undefined : Number(maxHits),
3666
},
3767
json: json as TQueryFnData,
3868
};

static/app/utils/api/apiOptions.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {expectTypeOf} from 'expect-type';
55
import {renderHookWithProviders, waitFor} from 'sentry-test/reactTestingLibrary';
66

77
import type {ApiResponse} from 'sentry/utils/api/apiFetch';
8-
import {apiOptions, selectJsonWithHeaders} from 'sentry/utils/api/apiOptions';
8+
import {apiOptions} from 'sentry/utils/api/apiOptions';
99

1010
type Promisable<T> = T | Promise<T>;
1111
type QueryFunctionResult<T> = Promisable<ApiResponse<T>>;
@@ -99,7 +99,7 @@ describe('apiOptions', () => {
9999
});
100100

101101
const {result} = renderHookWithProviders(() =>
102-
useQuery({...options, select: selectJsonWithHeaders})
102+
useQuery({...options, select: _ => _})
103103
);
104104

105105
await waitFor(() => expect(result.current.isPending).toBe(false));

static/app/utils/api/apiOptions.ts

Lines changed: 60 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1-
import {queryOptions, skipToken} from '@tanstack/react-query';
2-
import type {SkipToken} from '@tanstack/react-query';
3-
4-
import apiFetch, {type ApiResponse} from 'sentry/utils/api/apiFetch';
1+
import apiFetch, {apiFetchInfinite} from 'sentry/utils/api/apiFetch';
2+
import type {ApiResponse} from 'sentry/utils/api/apiFetch';
53
import getApiUrl from 'sentry/utils/api/getApiUrl';
64
import type {ExtractPathParams, OptionalPathParams} from 'sentry/utils/api/getApiUrl';
75
import type {KnownGetsentryApiUrls} from 'sentry/utils/api/knownGetsentryApiUrls';
86
import type {KnownSentryApiUrls} from 'sentry/utils/api/knownSentryApiUrls.generated';
9-
import type {ApiQueryKey, QueryKeyEndpointOptions} from 'sentry/utils/queryClient';
7+
import parseLinkHeader, {type ParsedHeader} from 'sentry/utils/parseLinkHeader';
8+
import {infiniteQueryOptions, queryOptions, skipToken} from 'sentry/utils/queryClient';
9+
import type {
10+
ApiQueryKey,
11+
InfiniteApiQueryKey,
12+
InfiniteData,
13+
QueryKeyEndpointOptions,
14+
SkipToken,
15+
} from 'sentry/utils/queryClient';
1016

1117
type KnownApiUrls = KnownGetsentryApiUrls | KnownSentryApiUrls;
1218

@@ -17,14 +23,6 @@ type PathParamOptions<TApiPath extends string> =
1723
? {path?: never}
1824
: {path: Record<ExtractPathParams<TApiPath>, string | number> | SkipToken};
1925

20-
/** @public **/
21-
export const selectJson = <TData>(data: ApiResponse<TData>) => data.json;
22-
23-
/** @public **/
24-
export const selectJsonWithHeaders = <TData>(
25-
data: ApiResponse<TData>
26-
): ApiResponse<TData> => data;
27-
2826
function _apiOptions<
2927
TManualData = never,
3028
TApiPath extends KnownApiUrls = KnownApiUrls,
@@ -38,14 +36,7 @@ function _apiOptions<
3836
? [Options & {path?: never}]
3937
: [Options & PathParamOptions<TApiPath>]
4038
) {
41-
const url = getApiUrl(
42-
path,
43-
...([
44-
{
45-
path: pathParams,
46-
},
47-
] as OptionalPathParams<TApiPath>)
48-
);
39+
const url = getApiUrl(path, ...([{path: pathParams}] as OptionalPathParams<TApiPath>));
4940

5041
return queryOptions({
5142
queryKey:
@@ -55,7 +46,46 @@ function _apiOptions<
5546
queryFn: pathParams === skipToken ? skipToken : apiFetch<TActualData>,
5647
enabled: pathParams !== skipToken,
5748
staleTime,
58-
select: selectJson,
49+
select: data => data.json,
50+
});
51+
}
52+
53+
function parsePageParam<TQueryFnData = unknown>(dir: 'previous' | 'next') {
54+
return ({headers}: ApiResponse<TQueryFnData>) => {
55+
const parsed = parseLinkHeader(headers.Link ?? null);
56+
return parsed[dir]?.results ? parsed[dir] : null;
57+
};
58+
}
59+
60+
function _apiOptionsInfinite<
61+
TManualData = never,
62+
TApiPath extends KnownApiUrls = KnownApiUrls,
63+
// todo: infer the actual data type from the ApiMapping
64+
TActualData = TManualData,
65+
>(
66+
path: TApiPath,
67+
...[
68+
{staleTime, path: pathParams, ...options},
69+
]: ExtractPathParams<TApiPath> extends never
70+
? [Options & {path?: never}]
71+
: [Options & PathParamOptions<TApiPath>]
72+
) {
73+
const url = getApiUrl(path, ...([{path: pathParams}] as OptionalPathParams<TApiPath>));
74+
75+
return infiniteQueryOptions({
76+
queryKey:
77+
Object.keys(options).length > 0
78+
? (['infinite', url, options] as InfiniteApiQueryKey)
79+
: (['infinite', url] as InfiniteApiQueryKey),
80+
queryFn: pathParams === skipToken ? skipToken : apiFetchInfinite<TActualData>,
81+
getPreviousPageParam: parsePageParam('previous'),
82+
getNextPageParam: parsePageParam('next'),
83+
initialPageParam: undefined,
84+
enabled: pathParams !== skipToken,
85+
staleTime,
86+
select: (
87+
data: InfiniteData<ApiResponse<TActualData>, null | undefined | ParsedHeader>
88+
) => data.pages.map(page => page.json),
5989
});
6090
}
6191

@@ -67,4 +97,12 @@ export const apiOptions = {
6797
options: Options & PathParamOptions<TApiPath>
6898
) =>
6999
_apiOptions<TManualData, TApiPath>(path, options as never),
100+
101+
asInfinite:
102+
<TManualData>() =>
103+
<TApiPath extends KnownApiUrls = KnownApiUrls>(
104+
path: TApiPath,
105+
options: Options & PathParamOptions<TApiPath>
106+
) =>
107+
_apiOptionsInfinite<TManualData, TApiPath>(path, options as never),
70108
};

0 commit comments

Comments
 (0)