From ad8df52b738bde30ace3b487858c6abef780417c Mon Sep 17 00:00:00 2001 From: Oliver Date: Mon, 18 Sep 2023 21:07:01 +1000 Subject: [PATCH] [PUI] Instance hook (#5564) * Custom hook for fetching instance data from the server * Refactor pages to use new hookr * Allow useInstance hook to handle parameters --- src/frontend/src/hooks/UseInstance.tsx | 58 +++++++++++++++++++ src/frontend/src/pages/build/BuildDetail.tsx | 33 +++-------- .../src/pages/part/CategoryDetail.tsx | 28 ++------- src/frontend/src/pages/part/PartDetail.tsx | 44 +++----------- 4 files changed, 81 insertions(+), 82 deletions(-) create mode 100644 src/frontend/src/hooks/UseInstance.tsx diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx new file mode 100644 index 0000000000..7da5eaeaf1 --- /dev/null +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -0,0 +1,58 @@ +import { useQuery } from '@tanstack/react-query'; +import { useCallback, useState } from 'react'; + +import { api } from '../App'; + +/** + * Custom hook for loading a single instance of an instance from the API + * + * - Queries the API for a single instance of an object, and returns the result. + * - Provides a callback function to refresh the instance + * + * To use this hook: + * const { instance, refreshInstance } = useInstance(url: string, pk: number) + */ +export function useInstance( + url: string, + pk: string | undefined, + params: any = {} +) { + const [instance, setInstance] = useState({}); + + const instanceQuery = useQuery({ + queryKey: ['instance', url, pk, params], + enabled: pk != null && pk != undefined && pk.length > 0, + queryFn: async () => { + return api + .get(url + pk + '/', { + params: params + }) + .then((response) => { + switch (response.status) { + case 200: + setInstance(response.data); + return response.data; + default: + setInstance({}); + return null; + } + }) + .catch((error) => { + setInstance({}); + console.error(`Error fetching instance ${url}${pk}:`, error); + return null; + }); + }, + refetchOnMount: false, + refetchOnWindowFocus: false + }); + + const refreshInstance = useCallback( + function () { + instanceQuery.refetch(); + }, + [instanceQuery] + ); + + return { instance, refreshInstance, instanceQuery }; +} diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index 01e600f19a..f400862af0 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -26,6 +26,7 @@ import { AttachmentTable } from '../../components/tables/AttachmentTable'; import { BuildOrderTable } from '../../components/tables/build/BuildOrderTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable'; import { NotesEditor } from '../../components/widgets/MarkdownEditor'; +import { useInstance } from '../../hooks/UseInstance'; /** * Detail page for a single Build Order @@ -33,30 +34,12 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor'; export default function BuildDetail() { const { id } = useParams(); - // Build data - const [build, setBuild] = useState({}); - - useEffect(() => { - setBuild({}); - }, [id]); - - // Query hook for fetching build data - const buildQuery = useQuery(['build', id ?? -1], async () => { - let url = `/build/${id}/`; - - return api - .get(url, { - params: { - part_detail: true - } - }) - .then((response) => { - setBuild(response.data); - }) - .catch((error) => { - console.error(error); - setBuild({}); - }); + const { + instance: build, + refreshInstance, + instanceQuery + } = useInstance('/build/', id, { + part_detail: true }); const buildPanels: PanelType[] = useMemo(() => { @@ -162,7 +145,7 @@ export default function BuildDetail() { ]} actions={[]} /> - + diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index 512b4d6533..9a554635cf 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -15,6 +15,7 @@ import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { PartCategoryTable } from '../../components/tables/part/PartCategoryTable'; import { PartListTable } from '../../components/tables/part/PartTable'; +import { useInstance } from '../../hooks/UseInstance'; /** * Detail view for a single PartCategory instance. @@ -24,27 +25,10 @@ import { PartListTable } from '../../components/tables/part/PartTable'; export default function CategoryDetail({}: {}) { const { id } = useParams(); - const [category, setCategory] = useState({}); - - useEffect(() => { - setCategory({}); - }, [id]); - - const categoryQuery = useQuery({ - enabled: id != null && id != undefined, - queryKey: ['category', id], - queryFn: async () => { - return api - .get(`/part/category/${id}/`) - .then((response) => { - setCategory(response.data); - return response.data; - }) - .catch((error) => { - console.error('Error fetching category data:', error); - }); - } - }); + const { instance: category, refreshInstance } = useInstance( + '/part/category/', + id + ); const categoryPanels: PanelType[] = useMemo( () => [ @@ -90,7 +74,7 @@ export default function CategoryDetail({}: {}) { title={t`Part Category`} detail={{category.name ?? 'Top level'}} breadcrumbs={ - id + category.pk ? [ { name: t`Parts`, url: '/part' }, { name: '...', url: '' }, diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index d49746925a..5eb8baed37 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,13 +1,5 @@ import { t } from '@lingui/macro'; -import { - Alert, - Button, - Group, - LoadingOverlay, - Space, - Stack, - Text -} from '@mantine/core'; +import { Alert, Button, LoadingOverlay, Stack, Text } from '@mantine/core'; import { IconBuilding, IconCurrencyDollar, @@ -41,6 +33,7 @@ import { NotesEditor } from '../../components/widgets/MarkdownEditor'; import { editPart } from '../../functions/forms/PartForms'; +import { useInstance } from '../../hooks/UseInstance'; /** * Detail view for a single Part instance @@ -48,12 +41,11 @@ import { editPart } from '../../functions/forms/PartForms'; export default function PartDetail() { const { id } = useParams(); - // Part data - const [part, setPart] = useState({}); - - useEffect(() => { - setPart({}); - }, [id]); + const { + instance: part, + refreshInstance, + instanceQuery + } = useInstance('/part/', id); // Part data panels (recalculate when part data changes) const partPanels: PanelType[] = useMemo(() => { @@ -153,22 +145,6 @@ export default function PartDetail() { ]; }, [part]); - // Query hook for fetching part data - const partQuery = useQuery(['part', id], async () => { - let url = `/part/${id}/`; - - return api - .get(url) - .then((response) => { - setPart(response.data); - return response.data; - }) - .catch((error) => { - setPart({}); - return null; - }); - }); - function partAttachmentsTab(): React.ReactNode { return ( { - partQuery.refetch(); - } + callback: refreshInstance }) } > @@ -236,7 +210,7 @@ export default function PartDetail() { ]} /> - +