chore(site): remove user search service (#9939)

This commit is contained in:
Bruno Quaresma
2023-10-02 15:24:28 -03:00
committed by GitHub
parent 42e25740eb
commit 9e1e365b32
4 changed files with 83 additions and 145 deletions

View File

@ -194,9 +194,12 @@ export const getTokenConfig = async (): Promise<TypesGen.TokenConfig> => {
export const getUsers = async ( export const getUsers = async (
options: TypesGen.UsersRequest, options: TypesGen.UsersRequest,
signal?: AbortSignal,
): Promise<TypesGen.GetUsersResponse> => { ): Promise<TypesGen.GetUsersResponse> => {
const url = getURLWithSearchParams("/api/v2/users", options); const url = getURLWithSearchParams("/api/v2/users", options);
const response = await axios.get<TypesGen.GetUsersResponse>(url.toString()); const response = await axios.get<TypesGen.GetUsersResponse>(url.toString(), {
signal,
});
return response.data; return response.data;
}; };

View File

@ -1,11 +1,15 @@
import { QueryClient } from "@tanstack/react-query"; import { QueryClient, QueryOptions } from "@tanstack/react-query";
import * as API from "api/api"; import * as API from "api/api";
import { UpdateUserPasswordRequest, UsersRequest } from "api/typesGenerated"; import {
GetUsersResponse,
UpdateUserPasswordRequest,
UsersRequest,
} from "api/typesGenerated";
export const users = (req: UsersRequest) => { export const users = (req: UsersRequest): QueryOptions<GetUsersResponse> => {
return { return {
queryKey: ["users", req], queryKey: ["users", req],
queryFn: () => API.getUsers(req), queryFn: ({ signal }) => API.getUsers(req, signal),
}; };
}; };

View File

@ -2,21 +2,15 @@ import CircularProgress from "@mui/material/CircularProgress";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import TextField from "@mui/material/TextField"; import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete"; import Autocomplete from "@mui/material/Autocomplete";
import { useMachine } from "@xstate/react";
import { User } from "api/typesGenerated"; import { User } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar"; import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/AvatarData/AvatarData"; import { AvatarData } from "components/AvatarData/AvatarData";
import { import { ChangeEvent, ComponentProps, FC, useState } from "react";
ChangeEvent,
ComponentProps,
FC,
useEffect,
useRef,
useState,
} from "react";
import { searchUserMachine } from "xServices/users/searchUserXService";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import { useDebouncedFunction } from "hooks/debounce"; import { useDebouncedFunction } from "hooks/debounce";
import { useQuery } from "@tanstack/react-query";
import { users } from "api/queries/users";
import { prepareQuery } from "utils/filters";
export type UserAutocompleteProps = { export type UserAutocompleteProps = {
value: User | null; value: User | null;
@ -34,53 +28,57 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
size = "small", size = "small",
}) => { }) => {
const styles = useStyles(); const styles = useStyles();
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false); const [autoComplete, setAutoComplete] = useState<{
const [searchState, sendSearch] = useMachine(searchUserMachine); value: string;
const { searchResults } = searchState.context; open: boolean;
}>({
value: value?.email ?? "",
open: false,
});
const usersQuery = useQuery({
...users({
q: prepareQuery(encodeURI(autoComplete.value)),
limit: 25,
}),
enabled: autoComplete.open,
keepPreviousData: true,
});
// Seed list of options on the first page load if a user passes in a value. const { debounced: debouncedInputOnChange } = useDebouncedFunction(
// Since some organizations have long lists of users, we do not want to load
// all options on page load.
const onMountRef = useRef(value);
useEffect(() => {
const mountValue = onMountRef.current;
if (mountValue) {
sendSearch("SEARCH", { query: mountValue.email });
}
// This isn't in XState's docs, but its source code guarantees that the
// memory reference of sendSearch will stay stable across renders. This
// useEffect call will behave like an on-mount effect and will not ever need
// to resynchronize
}, [sendSearch]);
const { debounced: debouncedOnChange } = useDebouncedFunction(
(event: ChangeEvent<HTMLInputElement>) => { (event: ChangeEvent<HTMLInputElement>) => {
sendSearch("SEARCH", { query: event.target.value }); setAutoComplete((state) => ({
...state,
value: event.target.value,
}));
}, },
1000, 750,
); );
return ( return (
<Autocomplete <Autocomplete
noOptionsText="Start typing to search..." // Since the values are filtered by the API we don't need to filter them
// in the FE because it can causes some mismatches.
filterOptions={(user) => user}
noOptionsText="No users found"
className={className} className={className}
options={searchResults ?? []} options={usersQuery.data?.users ?? []}
loading={searchState.matches("searching")} loading={usersQuery.isLoading}
value={value} value={value}
id="user-autocomplete" id="user-autocomplete"
open={isAutocompleteOpen} open={autoComplete.open}
onOpen={() => { onOpen={() => {
setIsAutocompleteOpen(true); setAutoComplete((state) => ({
...state,
open: true,
}));
}} }}
onClose={() => { onClose={() => {
setIsAutocompleteOpen(false); setAutoComplete({
value: value?.email ?? "",
open: false,
});
}} }}
onChange={(_, newValue) => { onChange={(_, newValue) => {
if (newValue === null) {
sendSearch("CLEAR_RESULTS");
}
onChange(newValue); onChange(newValue);
}} }}
isOptionEqualToValue={(option: User, value: User) => isOptionEqualToValue={(option: User, value: User) =>
@ -97,7 +95,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
</Box> </Box>
)} )}
renderInput={(params) => ( renderInput={(params) => (
<>
<TextField <TextField
{...params} {...params}
fullWidth fullWidth
@ -107,7 +104,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
className={styles.textField} className={styles.textField}
InputProps={{ InputProps={{
...params.InputProps, ...params.InputProps,
onChange: debouncedOnChange, onChange: debouncedInputOnChange,
startAdornment: value && ( startAdornment: value && (
<Avatar size="sm" src={value.avatar_url}> <Avatar size="sm" src={value.avatar_url}>
{value.username} {value.username}
@ -115,7 +112,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
), ),
endAdornment: ( endAdornment: (
<> <>
{searchState.matches("searching") ? ( {usersQuery.isFetching && autoComplete.open ? (
<CircularProgress size={16} /> <CircularProgress size={16} />
) : null} ) : null}
{params.InputProps.endAdornment} {params.InputProps.endAdornment}
@ -129,7 +126,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
shrink: true, shrink: true,
}} }}
/> />
</>
)} )}
/> />
); );

View File

@ -1,65 +0,0 @@
import { getUsers } from "api/api";
import { User } from "api/typesGenerated";
import { queryToFilter } from "utils/filters";
import { assign, createMachine } from "xstate";
export type AutocompleteEvent =
| { type: "SEARCH"; query: string }
| { type: "CLEAR_RESULTS" };
export const searchUserMachine = createMachine(
{
id: "searchUserMachine",
predictableActionArguments: true,
schema: {
context: {} as {
searchResults?: User[];
},
events: {} as AutocompleteEvent,
services: {} as {
searchUsers: {
data: User[];
};
},
},
context: {
searchResults: [],
},
tsTypes: {} as import("./searchUserXService.typegen").Typegen0,
initial: "idle",
states: {
idle: {
on: {
SEARCH: "searching",
CLEAR_RESULTS: {
actions: ["clearResults"],
target: "idle",
},
},
},
searching: {
invoke: {
src: "searchUsers",
onDone: {
target: "idle",
actions: ["assignSearchResults"],
},
},
},
},
},
{
services: {
searchUsers: async (_, { query }) =>
(await getUsers(queryToFilter(query))).users,
},
actions: {
assignSearchResults: assign({
searchResults: (_, { data }) => data,
}),
clearResults: assign({
searchResults: (_) => undefined,
}),
},
},
);