diff --git a/frontend/src/components/routing/ViewSetRouter.tsx b/frontend/src/components/routing/ViewSetRouter.tsx index 8c86558..b8b9b04 100644 --- a/frontend/src/components/routing/ViewSetRouter.tsx +++ b/frontend/src/components/routing/ViewSetRouter.tsx @@ -10,7 +10,7 @@ import {ResourceRouter} from "./ResourceRouter" export interface ViewSetRouterProps { - viewSet: ManagedViewSet, + viewSet?: ManagedViewSet, pathSegment: keyof ParsedPath, pkKey: keyof Resource // Don't ever dream of typing this. @@ -20,13 +20,17 @@ export interface ViewSetRouterProps { } -export function ViewSetRouter({viewSet, unselectedRoute, selectedRoute, pathSegment, pkKey}: ViewSetRouterProps): JSX.Element { +export function ViewSetRouter({viewSet, unselectedRoute: UnselectedRoute, selectedRoute: SelectedRoute, pathSegment, pkKey}: ViewSetRouterProps): JSX.Element { const path = useSophonPath() const pk = path?.[pathSegment] - const selection = pk ? viewSet.resources?.find(res => res.value[pkKey] === pk) : undefined - const UnselectedRoute = unselectedRoute - const SelectedRoute = selectedRoute + if(viewSet === undefined) { + return ( + + + + ) + } // If an error happens, display it in a ErrorBox if(viewSet.error) { @@ -35,7 +39,7 @@ export function ViewSetRouter({viewSet, unselec ) } - // If the viewset is loading, display a loading message + // If the viewset is still loading, display a loading message if(viewSet.resources === null) { return ( @@ -44,6 +48,8 @@ export function ViewSetRouter({viewSet, unselec ) } + const selection = pk ? viewSet.resources?.find(res => res.value[pkKey] === pk) : undefined + return ( Pr // Public interfaces +/** + * A ViewSet managed by {@link ManagedViewSet}. + */ export interface ManagedViewSet { + /** + * Whether the whole ViewSet is busy performing an operation (`refresh`, `create`, `command`) or not. + */ busy: boolean, + + /** + * The last error that occourred during an operation "poisoning" the whole ViewSet (`refresh`). + */ error: Error | null, + + /** + * The last error that occourred during an operation on the ViewSet (`create`, `command`). + */ operationError: Error | null, + /** + * The full array of the resources of the ViewSet. + */ resources: ManagedResource[] | null, + /** + * The function to call to `refresh` the whole ViewSet (re-`list` all resources). + */ refresh: ManagedRefresh, + + /** + * The function to call to `create` a new resource. + */ create: ManagedCreate, + + /** + * The function to call to run a `command` on the whole ViewSet. + */ command: ManagedCommand, } +/** + * A single resource managed by {@link useManagedViewSet}. + */ export interface ManagedResource { + /** + * The value of the resource. + */ value: Resource, + + /** + * Whether the resource is busy performing an operation (`update`, `destroy`, `action`) or not. + */ busy: boolean, + + /** + * The last error that occourred during an operation on the resource. + */ error: Error | null, + /** + * The function to call to `update` the resource. + */ update: ManagedUpdateDetails + + /** + * The function to call to `destroy` the resource. + */ destroy: ManagedDestroyDetails + + /** + * The function to run an `action` on the resource. + */ action: ManagedActionDetails, } // Reducer state +/** + * State for {@link reducerManagedViewSet}. + */ export interface ManagedReducerState { firstRun: boolean, @@ -61,6 +117,9 @@ export interface ManagedReducerState { } +/** + * Action for {@link reducerManagedViewSet}. + */ export interface ManagedReducerAction { type: `${"refresh" | "create" | "command" | "update" | "destroy" | "action"}.${"start" | "success" | "error"}`, value?: any, @@ -68,6 +127,9 @@ export interface ManagedReducerAction { } +/** + * Reducer for {@link useManagedViewSet}. + */ function reducerManagedViewSet(state: ManagedReducerState, action: ManagedReducerAction): ManagedReducerState { switch(action.type) { @@ -221,7 +283,16 @@ function reducerManagedViewSet(state: ManagedReducerState, a } -export function useManagedViewSet(baseRoute: string, pkKey: keyof Resource, refreshOnMount: boolean = true): ManagedViewSet { +/** + * Hook that provides an high-level interface for interacting with a Django ViewSet ({@link ManagedViewSet}). + * + * Returns `undefined` outside an {@link InstanceProvider}. + * + * @param baseRoute - The path to the ViewSet with a trailing slash. + * @param pkKey - The key of the returned resource that represents the primary key. + * @param refreshOnMount - Whether `refresh` should be automatically called on initialization or not. + */ +export function useManagedViewSet(baseRoute: string, pkKey: keyof Resource, refreshOnMount: boolean = true): ManagedViewSet | undefined { const viewset = useViewSet(baseRoute) @@ -250,7 +321,7 @@ export function useManagedViewSet(baseRoute: st let response: Resource[] try { - response = await viewset.list() + response = await viewset!.list() } catch(err) { @@ -288,7 +359,7 @@ export function useManagedViewSet(baseRoute: st let response: Resource try { - response = await viewset.create({data}) + response = await viewset!.create({data}) } catch(err) { @@ -326,7 +397,7 @@ export function useManagedViewSet(baseRoute: st let response: Resource[] try { - response = await viewset.command({url: `${baseRoute}${cmd}/`, data, method}) + response = await viewset!.command({url: `${baseRoute}${cmd}/`, data, method}) } catch(err) { @@ -374,7 +445,7 @@ export function useManagedViewSet(baseRoute: st let response: Resource try { - response = await viewset.update(pk, {data}) + response = await viewset!.update(pk, {data}) } catch(err) { @@ -421,7 +492,7 @@ export function useManagedViewSet(baseRoute: st }) try { - await viewset.destroy(pk) + await viewset!.destroy(pk) } catch(err) { @@ -468,7 +539,7 @@ export function useManagedViewSet(baseRoute: st let response: Resource try { - response = await viewset.action({url: `${baseRoute}${pk}/${act}/`, data, method}) + response = await viewset!.action({url: `${baseRoute}${pk}/${act}/`, data, method}) } catch(err) { @@ -514,18 +585,35 @@ export function useManagedViewSet(baseRoute: st useEffect( () => { - if(!refreshOnMount) return - if(!state.firstRun) return - if(state.busy) return - if(state.error) return - if(state.resources) return + if(!refreshOnMount) { + return + } + if(!viewset) { + return + } + if(!state.firstRun) { + return + } + if(state.busy) { + return + } + if(state.error) { + return + } + if(state.resources) { + return + } // noinspection JSIgnoredPromiseFromCall refresh() }, - [refresh, state, refreshOnMount] + [refresh, state, refreshOnMount], ) + if(!viewset) { + return undefined + } + return { busy: state.busy, error: state.error, diff --git a/frontend/src/hooks/useViewSet.ts b/frontend/src/hooks/useViewSet.ts index 988759f..14d5f6b 100644 --- a/frontend/src/hooks/useViewSet.ts +++ b/frontend/src/hooks/useViewSet.ts @@ -78,20 +78,20 @@ export interface ViewSet { * * Useful for performing low-level operations on a ViewSet. * + * Returns `undefined` outside an {@link InstanceProvider}. + * * @param baseRoute - The path to the ViewSet with a trailing slash. */ -export function useViewSet(baseRoute: string): ViewSet { +export function useViewSet(baseRoute: string): ViewSet | undefined { const api = useAuthorizedAxios() const command: ViewSetCommand = React.useCallback( async (config) => { - if (!api) throw new Error("useViewSet called while the Sophon instance was undefined.") - let nextUrl: string | null = config.url let resources: Resource[] = [] - while (nextUrl !== null) { - const response: AxiosResponse> = await api.request>({...config, url: nextUrl}) + while(nextUrl !== null) { + const response: AxiosResponse> = await api!.request>({...config, url: nextUrl}) nextUrl = response.data.next resources = [...resources, ...response.data.results] } @@ -103,9 +103,7 @@ export function useViewSet(baseRoute: string): ViewSet { const action: ViewSetAction = React.useCallback( async (config) => { - if (!api) throw new Error("useViewSet called while the Sophon instance was undefined.") - - const response = await api.request(config) + const response = await api!.request(config) return response.data }, [api] @@ -151,5 +149,9 @@ export function useViewSet(baseRoute: string): ViewSet { [action, baseRoute], ) + if(!api) { + return undefined + } + return {command, action, list, retrieve, create, update, destroy} }