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 (
options: TypesGen.UsersRequest,
signal?: AbortSignal,
): Promise<TypesGen.GetUsersResponse> => {
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;
};

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 { 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 {
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 TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import { useMachine } from "@xstate/react";
import { User } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { AvatarData } from "components/AvatarData/AvatarData";
import {
ChangeEvent,
ComponentProps,
FC,
useEffect,
useRef,
useState,
} from "react";
import { searchUserMachine } from "xServices/users/searchUserXService";
import { ChangeEvent, ComponentProps, FC, useState } from "react";
import Box from "@mui/material/Box";
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 = {
value: User | null;
@ -34,53 +28,57 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
size = "small",
}) => {
const styles = useStyles();
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
const [searchState, sendSearch] = useMachine(searchUserMachine);
const { searchResults } = searchState.context;
const [autoComplete, setAutoComplete] = useState<{
value: string;
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.
// 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(
const { debounced: debouncedInputOnChange } = useDebouncedFunction(
(event: ChangeEvent<HTMLInputElement>) => {
sendSearch("SEARCH", { query: event.target.value });
setAutoComplete((state) => ({
...state,
value: event.target.value,
}));
},
1000,
750,
);
return (
<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}
options={searchResults ?? []}
loading={searchState.matches("searching")}
options={usersQuery.data?.users ?? []}
loading={usersQuery.isLoading}
value={value}
id="user-autocomplete"
open={isAutocompleteOpen}
open={autoComplete.open}
onOpen={() => {
setIsAutocompleteOpen(true);
setAutoComplete((state) => ({
...state,
open: true,
}));
}}
onClose={() => {
setIsAutocompleteOpen(false);
setAutoComplete({
value: value?.email ?? "",
open: false,
});
}}
onChange={(_, newValue) => {
if (newValue === null) {
sendSearch("CLEAR_RESULTS");
}
onChange(newValue);
}}
isOptionEqualToValue={(option: User, value: User) =>
@ -97,7 +95,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
</Box>
)}
renderInput={(params) => (
<>
<TextField
{...params}
fullWidth
@ -107,7 +104,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
className={styles.textField}
InputProps={{
...params.InputProps,
onChange: debouncedOnChange,
onChange: debouncedInputOnChange,
startAdornment: value && (
<Avatar size="sm" src={value.avatar_url}>
{value.username}
@ -115,7 +112,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
),
endAdornment: (
<>
{searchState.matches("searching") ? (
{usersQuery.isFetching && autoComplete.open ? (
<CircularProgress size={16} />
) : null}
{params.InputProps.endAdornment}
@ -129,7 +126,6 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
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,
}),
},
},
);