improve FilteredConnection filter types (#63672)

This gives a bit more type-safety when using FilteredConnection filters.

## Test plan

CI
This commit is contained in:
Quinn Slack 2024-07-05 08:59:28 -05:00 committed by GitHub
parent 73881aef18
commit b0f3ba5f35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 34 additions and 17 deletions

View File

@ -8,8 +8,13 @@ import styles from './FilterControl.module.scss'
/**
* A filter to display next to the search input field.
* @template K The IDs of all filters ({@link Filter.id} values).
* @template A The type of option args ({@link Filter.options} {@link FilterOption.args} values).
*/
export interface Filter {
export interface Filter<
K extends string = string,
A extends Record<string, string | number | boolean | null> = Record<string, string | number | boolean | null>
> {
/** The UI label for the filter. */
label: string
@ -20,7 +25,7 @@ export interface Filter {
* The URL query parameter name for this filter (conventionally the label, lowercased and
* without spaces and punctuation).
*/
id: string
id: K
/** An optional tooltip to display for this filter. */
tooltip?: string
@ -28,13 +33,16 @@ export interface Filter {
/**
* All of the possible values for this filter that the user can select.
*/
options: FilterOption[]
options: FilterOption<A>[]
}
/**
* An option that the user can select for a filter ({@link Filter}).
* @template A The type of option args ({@link Filter.options} {@link FilterOption.args} values).
*/
export interface FilterOption {
export interface FilterOption<
A extends Record<string, string | number | boolean | null> = Record<string, string | number | boolean | null>
> {
/**
* The value (corresponding to the key in {@link Filter.id}) if this option is chosen. For
* example, if a filter has {@link Filter.id} of `sort` and the user selects a
@ -44,13 +52,14 @@ export interface FilterOption {
value: string
label: string
tooltip?: string
args: { [name: string]: string | number | boolean }
args: A
}
/**
* The values of all filters, keyed by the filter ID ({@link Filter.id}).
* @template K The IDs of all filters ({@link Filter.id} values).
*/
export interface FilterValues extends Record<string, FilterOption['value'] | null> {}
export type FilterValues<K extends string = string> = Record<K, FilterOption['value'] | null>
interface FilterControlProps {
/** All filters. */

View File

@ -113,7 +113,11 @@ interface FilteredConnectionProps<C extends Connection<N>, N, NP = {}, HP = {}>
queryConnection: (args: FilteredConnectionQueryArguments) => Observable<C>
/** Called when the queryConnection Observable emits. */
onUpdate?: (value: C | ErrorLike | undefined, query: string, activeValues: FilteredConnectionArgs) => void
onUpdate?: (
value: C | ErrorLike | undefined,
query: string,
activeValues: Record<string, string | number | boolean | null>
) => void
/**
* Set to true when the GraphQL response is expected to emit an `PageInfo.endCursor` value when
@ -660,8 +664,15 @@ class InnerFilteredConnection<N, NP = {}, HP = {}, C extends Connection<N> = Con
private buildArgs = buildFilterArgs
}
export const buildFilterArgs = (filters: Filter[], filterValues: FilterValues): FilteredConnectionArgs => {
let args: FilteredConnectionArgs = {}
/**
* @template K The IDs of all filters ({@link Filter.id} values).
* @template A The type of option args ({@link Filter.options} {@link FilterOption.args} values).
*/
export function buildFilterArgs<
K extends string = string,
A extends Record<string, string | number | boolean | null> = Record<string, string | number | boolean | null>
>(filters: Filter<K, A>[], filterValues: FilterValues<K>): A {
let args = {} as unknown as A
for (const [filterID, value] of Object.entries(filterValues)) {
if (value === undefined) {
continue
@ -684,7 +695,3 @@ export const resetFilteredConnectionURLQuery = (parameters: URLSearchParams): vo
parameters.delete('first')
parameters.delete('after')
}
export interface FilteredConnectionArgs {
[name: string]: string | number | boolean
}

View File

@ -1,9 +1,9 @@
import { useCallback, useMemo } from 'react'
import type { ApolloError, WatchQueryFetchPolicy } from '@apollo/client'
import { useNavigate, useLocation } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import { type GraphQLResult, useQuery } from '@sourcegraph/http-client'
import { useQuery, type GraphQLResult } from '@sourcegraph/http-client'
import { asGraphQLResult } from '../utils'
@ -59,9 +59,11 @@ interface UsePaginatedConnectionConfig<TResult> {
pollInterval?: number
}
export type PaginationKeys = 'first' | 'last' | 'before' | 'after'
interface UsePaginatedConnectionParameters<TResult, TVariables extends PaginatedConnectionQueryArguments, TNode> {
query: string
variables: Omit<TVariables, 'first' | 'last' | 'before' | 'after'>
variables: Omit<TVariables, PaginationKeys>
getConnection: (result: GraphQLResult<TResult>) => PaginatedConnection<TNode> | undefined
options?: UsePaginatedConnectionConfig<TResult>
}
@ -71,7 +73,6 @@ const DEFAULT_PAGE_SIZE = 20
/**
* Request a GraphQL connection query and handle pagination options.
* Valid queries should follow the connection specification at https://relay.dev/graphql/connections.htm
*
* @param query The GraphQL connection query
* @param variables The GraphQL connection variables
* @param getConnection A function that filters and returns the relevant data from the connection response.