Skip to content

Commit

Permalink
feat: Update uuid -> id; mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaquiery committed Apr 23, 2024
1 parent d05c5a9 commit 8f6d969
Show file tree
Hide file tree
Showing 25 changed files with 585 additions and 182 deletions.
4 changes: 2 additions & 2 deletions src/ClientCodeDemo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ function DatasetSelector({selectedDatasetIds, setSelectedDatasetIds}: {
value={selectedDatasetIds}
onChange={(e) => setSelectedDatasetIds(e.target.value as string[])}
renderValue={(selected) =>
query.results!.filter(f => selected.includes(String(f.uuid)))
query.results!.filter(f => selected.includes(String(f.id)))
.map(f => f.name as string)
.join(', ')
}
>
{query.results.map((dataset) => (
<MenuItem value={String(dataset.uuid)} key={String(dataset.uuid)}>{String(dataset.name)}</MenuItem>
<MenuItem value={String(dataset.id)} key={String(dataset.id)}>{String(dataset.name)}</MenuItem>
))}
</Select>
</FormControl>
Expand Down
2 changes: 1 addition & 1 deletion src/Components/AttachmentUploadContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function AttachmentUploadContextProvider({children}: PropsWithChi
},
{
onSuccess: (data: AxiosResponse<ArbitraryFile>) => {
queryClient.setQueryData([LOOKUP_KEYS.ARBITRARY_FILE, data.data.uuid], data.data)
queryClient.setQueryData([LOOKUP_KEYS.ARBITRARY_FILE, data.data.id], data.data)
queryClient.invalidateQueries([LOOKUP_KEYS.ARBITRARY_FILE, "list"])
}
}
Expand Down
6 changes: 2 additions & 4 deletions src/Components/CardActionBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import React, {ReactNode} from "react";
import Tooltip from "@mui/material/Tooltip";
import IconButton from "@mui/material/IconButton";
import EditIcon from "@mui/icons-material/Edit";
import DeleteIcon from "@mui/icons-material/Delete";
import UndoIcon from "@mui/icons-material/Undo";
import RedoIcon from "@mui/icons-material/Redo";
import SaveIcon from "@mui/icons-material/Save";
import CloseIcon from "@mui/icons-material/Close";
import Stack from "@mui/material/Stack";
import CountBadge from "./CountBadge";
Expand Down Expand Up @@ -138,7 +136,7 @@ export default function CardActionBar(props: CardActionBarProps) {
if (props.onEditSave!())
props.setEditing!(false)
}}>
<SaveIcon {...iconProps} color="success"/>
<ICONS.SAVE {...iconProps} color="success"/>
</IconButton>
</Tooltip>
{props.onUndo && <Tooltip title={`Undo`} arrow describeChild key="undo">
Expand Down Expand Up @@ -178,7 +176,7 @@ export default function CardActionBar(props: CardActionBarProps) {
>
<span>
<IconButton onClick={() => props.onDestroy && props.onDestroy()} disabled={!props.destroyable}>
<DeleteIcon className={clsx(classes.deleteIcon)} {...iconProps}/>
<ICONS.DELETE className={clsx(classes.deleteIcon)} {...iconProps}/>
</IconButton>
</span>
</Tooltip>
Expand Down
6 changes: 3 additions & 3 deletions src/Components/CurrentUserContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {createContext, ReactNode, useContext, useState} from "react";
import {Configuration, KnoxUser, LoginApi, User}from "@battery-intelligence-lab/galv";
import {useMutation, useQueryClient} from "@tanstack/react-query";
import {AxiosResponse} from "axios";
import {AxiosError, AxiosResponse} from "axios";
import axios from "axios";
import {useSnackbarMessenger} from "./SnackbarMessengerContext";
import Button from "@mui/material/Button";
Expand Down Expand Up @@ -39,10 +39,10 @@ export default function CurrentUserContextProvider({children}: {children: ReactN
password
})
const api_handler = new LoginApi(get_config())
const Login = useMutation(
const Login = useMutation<AxiosResponse<KnoxUser>, AxiosError, void>(
() => api_handler.loginCreate.bind(new LoginApi(get_config()))(),
{
onSuccess: (data: AxiosResponse<KnoxUser>) => {
onSuccess: (data) => {
window.localStorage.setItem('user', JSON.stringify(data.data))
setUser(data.data as unknown as LoginUser)
queryClient.removeQueries({predicate: () => true})
Expand Down
225 changes: 214 additions & 11 deletions src/Components/FetchResourceContext.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import {createContext, ReactNode, useContext} from "react";
import {useCurrentUser} from "./CurrentUserContext";
import {API_HANDLERS, API_SLUGS, AutocompleteKey, DISPLAY_NAMES, is_lookup_key, LookupKey} from "../constants";
import {
API_HANDLERS,
API_SLUGS,
AutocompleteKey,
DISPLAY_NAMES,
is_lookup_key,
LookupKey
} from "../constants";
import {AxiosError, AxiosResponse} from "axios";
import {
MutationFunction,
QueryFunction,
useInfiniteQuery,
type UseInfiniteQueryResult, useQuery,
type UseInfiniteQueryResult, useMutation, UseMutationOptions, UseMutationResult, useQuery,
useQueryClient, UseQueryOptions,
UseQueryResult
} from "@tanstack/react-query";
Expand All @@ -25,6 +33,30 @@ export type ListQueryResult<T> = UseInfiniteQueryResult & {
results: T[] | null | undefined
}

type RetrieveOptions<T extends BaseResource> = {
extra_query_options?: UseQueryOptions<AxiosResponse<T>, AxiosError>,
with_result?: (result: AxiosResponse<T>) => AxiosResponse<T>,
on_error?: (error: AxiosError) => AxiosResponse<T>|undefined
}
type UpdateTVariables<T extends BaseResource> = Partial<T> & {id: string|number}
type UpdateOptions<T extends BaseResource> = {
extra_query_options?: UseMutationOptions<AxiosResponse<T>, AxiosError>,
before_cache?: (result: AxiosResponse<T>) => AxiosResponse<T>,
after_cache?: (result: AxiosResponse<T>, variables: UpdateTVariables<T>) => void,
on_error?: (error: AxiosError, variables: UpdateTVariables<T>) => AxiosResponse<T>|undefined
}
type CreateOptions<T extends BaseResource> = {
extra_query_options?: UseMutationOptions<AxiosResponse<T>, AxiosError>,
before_cache?: (result: AxiosResponse<T>) => AxiosResponse<T>,
after_cache?: (result: AxiosResponse<T>, variables: Partial<T>) => void,
on_error?: (error: AxiosError, variables: Partial<T>) => AxiosResponse<T>|undefined
}
type DeleteOptions<T extends BaseResource> = {
extra_query_options?: UseMutationOptions<AxiosResponse<null>, AxiosError>,
after?: () => void,
on_error?: (error: AxiosError, variables: T) => void
}

export interface IFetchResourceContext {
// Returns null when lookup_key is undefined. Otherwise, returns undefined until data are fetched, then T[]
useListQuery: <T extends BaseResource>(
Expand All @@ -33,18 +65,31 @@ export interface IFetchResourceContext {
useRetrieveQuery: <T extends BaseResource>(
lookup_key: LookupKey,
resource_id: string|number,
options?: {
extra_query_options?: UseQueryOptions<AxiosResponse<T>, AxiosError>,
with_result?: (r: AxiosResponse<T>) => AxiosResponse<T>,
on_error?: (e: AxiosError) => AxiosResponse<T>|undefined
}
options?: RetrieveOptions<T>
) => UseQueryResult<AxiosResponse<T>, AxiosError>
useUpdateQuery: <T extends BaseResource>(
lookup_key: LookupKey,
options?: UpdateOptions<T>
) => UseMutationResult<AxiosResponse<T>, AxiosError, UpdateTVariables<T>>
useCreateQuery: <T extends BaseResource>(
lookup_key: LookupKey,
options?: CreateOptions<T>
) => UseMutationResult<AxiosResponse<T>, AxiosError, Partial<T>>
useDeleteQuery: <T extends BaseResource>(
lookup_key: LookupKey,
options?: DeleteOptions<T>
) => UseMutationResult<AxiosResponse<null>, AxiosError, T>
}

export const FetchResourceContext = createContext({} as IFetchResourceContext)

export const useFetchResource = () => useContext(FetchResourceContext)

const get_error_detail = (e: AxiosError) => e.response?.data?.detail ??
Object.entries(e.response?.data ?? {})
.map(([k, v]) => `${k}: ${v}`)
.join(', ')

export default function FetchResourceContextProvider({children}: {children: ReactNode}) {
const extract_limit_offset = (url: string|null|undefined) => {
const safe_number = (n: string | null) => n && !isNaN(parseInt(n))? parseInt(n) : undefined
Expand Down Expand Up @@ -82,7 +127,7 @@ export default function FetchResourceContextProvider({children}: {children: Reac
else
data = resource
queryClient.setQueryData(
[lookup_key, resource.uuid ?? resource.id ?? "no id in List response"],
[lookup_key, resource.id ?? "no id in List response"],
data
)
})
Expand Down Expand Up @@ -129,13 +174,13 @@ export default function FetchResourceContextProvider({children}: {children: Reac
const api_handler = new API_HANDLERS[lookup_key](config)
const get = api_handler[
`${API_SLUGS[lookup_key]}Retrieve` as keyof typeof api_handler
] as (uuid: string) => Promise<AxiosResponse<T>>
] as (id: string) => Promise<AxiosResponse<T>>

const after = options?.with_result? options.with_result : (r: AxiosResponse<T>) => r
const on_error_fn = options?.on_error? options.on_error : (e: AxiosError) => {
postSnackbarMessage({
message: `Error retrieving ${DISPLAY_NAMES[lookup_key]}/${resource_id}
(HTTP ${e.response?.status} - ${e.response?.statusText}): ${e.response?.data?.detail}`,
(HTTP ${e.response?.status} - ${e.response?.statusText}): ${get_error_detail(e)}`,
severity: 'error'
})
}
Expand All @@ -160,7 +205,165 @@ export default function FetchResourceContextProvider({children}: {children: Reac
return useQuery<AxiosResponse<T>, AxiosError>(query_options)
}

return <FetchResourceContext.Provider value={{useListQuery, useRetrieveQuery}}>
const useUpdateQuery: IFetchResourceContext["useUpdateQuery"] = <T extends BaseResource>(
lookup_key: LookupKey,
options?: UpdateOptions<T>
) => {
const queryClient = useQueryClient()
const {postSnackbarMessage} = useSnackbarMessenger()
const config = new Configuration({
basePath: process.env.VITE_GALV_API_BASE_URL,
accessToken: useCurrentUser().user?.token
})
const api_handler = new API_HANDLERS[lookup_key](config)
const partialUpdate = api_handler[
`${API_SLUGS[lookup_key]}PartialUpdate` as keyof typeof api_handler
] as (id: string, data: Partial<T>) => Promise<AxiosResponse<T>>

const pre_cache = options?.before_cache? options.before_cache : (r: AxiosResponse<T>) => r
// (r, v) => ({r, v}) does nothing except stop TS from complaining about unused variables
const post_cache = options?.after_cache?
options.after_cache : (r: AxiosResponse<T>, v: UpdateTVariables<T>) => ({r, v})
// Need v so TS recognises the function as callable with 2 arguments
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const on_error_fn = options?.on_error? options.on_error : (e: AxiosError, v: UpdateTVariables<T>) => {
postSnackbarMessage({
message: `Error updating ${DISPLAY_NAMES[lookup_key]}/${v.id ?? v.id}
(HTTP ${e.response?.status} - ${e.response?.statusText}): ${get_error_detail(e)}`,
severity: 'error'
})
}

const mutationFn: MutationFunction<AxiosResponse<T>, Partial<T>> =
(data: Partial<T>) => partialUpdate
.bind(api_handler)(String(data.id ?? data.id), data)
.then(pre_cache)

const mutation_options: UseMutationOptions<AxiosResponse<T>, AxiosError, Partial<T>> = {
mutationKey: [lookup_key, 'update'],
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
mutationFn: mutationFn,
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
onSuccess: (data: AxiosResponse<T>, variables: UpdateTVariables<T>) => {
// Update cache
const queryKey = [lookup_key, variables.id]
queryClient.setQueryData(queryKey, data)
// Invalidate list cache
queryClient.invalidateQueries([lookup_key, 'list'])
// Invalidate autocomplete cache
queryClient.invalidateQueries(['autocomplete'])
post_cache(data, variables)
},
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
onError: on_error_fn,
...options?.extra_query_options
}
return useMutation<AxiosResponse<T>, AxiosError, UpdateTVariables<T>>(mutation_options)
}

const useCreateQuery: IFetchResourceContext["useCreateQuery"] = <T extends BaseResource>(
lookup_key: LookupKey,
options?: CreateOptions<T>
) => {
const queryClient = useQueryClient()
const {postSnackbarMessage} = useSnackbarMessenger()
const config = new Configuration({
basePath: process.env.VITE_GALV_API_BASE_URL,
accessToken: useCurrentUser().user?.token
})
const api_handler = new API_HANDLERS[lookup_key](config)
const create = api_handler[
`${API_SLUGS[lookup_key]}Create` as keyof typeof api_handler
] as (data: Partial<T>) => Promise<AxiosResponse<T>>

const pre_cache = options?.before_cache? options.before_cache : (r: AxiosResponse<T>) => r
// (r, v) => ({r, v}) does nothing except stop TS from complaining about unused variables
const post_cache = options?.after_cache?
options.after_cache : (r: AxiosResponse<T>, v: Partial<T>) => ({r, v})
const on_error_fn = options?.on_error? options.on_error : (e: AxiosError, v: Partial<T>) => {
postSnackbarMessage({
message: `Error creating ${DISPLAY_NAMES[lookup_key]}
${v.name ?? v.title ?? v.identifier ?? v.model ?? v.username}
(HTTP ${e.response?.status} - ${e.response?.statusText}): ${get_error_detail(e)}`,
severity: 'error'
})
}

const mutationFn: MutationFunction<AxiosResponse<T>, Partial<T>> =
(data: Partial<T>) => create.bind(api_handler)(data).then(pre_cache)

const mutation_options: UseMutationOptions<AxiosResponse<T>, AxiosError, Partial<T>> = {
mutationKey: [lookup_key, 'create'],
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
mutationFn: mutationFn,
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
onSuccess: (data: AxiosResponse<T>, variables: Partial<T>) => {
// Update cache
const queryKey = [lookup_key, data.data.id ?? "no id in Create response"]
queryClient.setQueryData(queryKey, data)
// Invalidate list cache
queryClient.invalidateQueries([lookup_key, 'list'])
// Invalidate autocomplete cache
queryClient.invalidateQueries(['autocomplete'])
post_cache(data, variables)
},
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
onError: on_error_fn,
...options?.extra_query_options
}
return useMutation<AxiosResponse<T>, AxiosError, Partial<T>>(mutation_options)
}

const useDeleteQuery: IFetchResourceContext["useDeleteQuery"] = <T extends BaseResource>(
lookup_key: LookupKey,
options?: DeleteOptions<T>
) => {
const queryClient = useQueryClient()
const {postSnackbarMessage} = useSnackbarMessenger()
const config = new Configuration({
basePath: process.env.VITE_GALV_API_BASE_URL,
accessToken: useCurrentUser().user?.token
})
const api_handler = new API_HANDLERS[lookup_key](config)
const destroy = api_handler[
`${API_SLUGS[lookup_key]}Destroy` as keyof typeof api_handler
] as (id: string) => Promise<AxiosResponse<null>>

const on_error_fn = options?.on_error? options.on_error : (e: AxiosError, v: T) => {
postSnackbarMessage({
message: `Error deleting ${DISPLAY_NAMES[lookup_key]}/${v.id ?? v.id}
(HTTP ${e.response?.status} - ${e.response?.statusText}): ${get_error_detail(e)}`,
severity: 'error'
})
}

const mutationFn: MutationFunction<AxiosResponse<null>, T> =
(data: T) => destroy.bind(api_handler)(String(data.id))

const mutation_options: UseMutationOptions<AxiosResponse<null>, AxiosError, T> = {
mutationKey: [lookup_key, 'delete'],
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
mutationFn: mutationFn,
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
onSuccess: (data: AxiosResponse<null>, variables: T) => {
// Invalidate cache
queryClient.removeQueries([lookup_key, variables.id])
// Invalidate list cache
queryClient.invalidateQueries([lookup_key, 'list'])
// Invalidate autocomplete cache
queryClient.invalidateQueries(['autocomplete'])
if (options?.after) options.after()
},
// @ts-expect-error - TS incorrectly infers that TVariables can be of type void
onError: on_error_fn,
...options?.extra_query_options
}
return useMutation<AxiosResponse<null>, AxiosError, T>(mutation_options)
}

return <FetchResourceContext.Provider value={{
useListQuery, useRetrieveQuery, useUpdateQuery, useCreateQuery, useDeleteQuery
}}>
{children}
</FetchResourceContext.Provider>
}
Loading

0 comments on commit 8f6d969

Please sign in to comment.