import * as React from "react" import {useEffect, useMemo, useReducer} from "react" import {DjangoResource} from "../types/DjangoTypes" import {arrayExclude, arrayExtension} from "../utils/ArrayExtension" import {useViewSet} from "./useViewSet" // Function types type ManagedRefresh = () => Promise type ManagedCreate = (data: Partial) => Promise type ManagedCommand = (command: string, data: any) => Promise type ManagedUpdate = (index: number, data: Partial) => Promise type ManagedDestroy = (index: number) => Promise type ManagedAction = (index: number, act: string, data: any) => Promise type ManagedUpdateDetails = (data: Partial) => Promise type ManagedDestroyDetails = () => Promise type ManagedActionDetails = (act: string, data: any) => Promise // Public interfaces export interface ManagedViewSet { busy: boolean, error: Error | null, operationError: Error | null, resources: ManagedResource[] | null, refresh: ManagedRefresh, create: ManagedCreate, command: ManagedCommand, } export interface ManagedResource { value: Resource, busy: boolean, error: Error | null, update: ManagedUpdateDetails destroy: ManagedDestroyDetails action: ManagedActionDetails, } // Reducer state export interface ManagedReducerState { firstRun: boolean, busy: boolean, error: Error | null, operationError: Error | null, resources: Resource[] | null, resourceBusy: boolean[] | null, resourceError: (Error | null)[] | null, } export interface ManagedReducerAction { type: `${"refresh" | "create" | "command" | "update" | "destroy" | "action"}.${"start" | "success" | "error"}`, value?: any, index?: number, } function reducerManagedViewSet(state: ManagedReducerState, action: ManagedReducerAction): ManagedReducerState { switch(action.type) { case "refresh.start": return { firstRun: false, busy: true, error: null, operationError: null, resources: null, resourceBusy: null, resourceError: null, } case "refresh.success": return { ...state, busy: false, error: null, operationError: null, resources: action.value, resourceBusy: action.value.map(() => false), resourceError: action.value.map(() => null), } case "refresh.error": return { ...state, busy: false, error: action.value, } case "command.start": return { ...state, busy: true, } case "command.success": return { ...state, busy: false, error: null, operationError: null, resources: action.value, resourceBusy: action.value.map(() => false), resourceError: action.value.map(() => null), } case "command.error": return { ...state, busy: false, operationError: action.value, } case "create.start": return { ...state, busy: true, } case "create.success": return { ...state, busy: false, resources: [...state.resources!, action.value], resourceBusy: [...state.resourceBusy!, false], resourceError: [...state.resourceError!, null], } case "create.error": return { ...state, busy: false, operationError: action.value, } case "update.start": return { ...state, operationError: null, resourceBusy: arrayExtension(state.resourceBusy!, action.index!, true), } case "update.success": return { ...state, busy: false, resources: arrayExtension(state.resources!, action.index!, action.value), resourceBusy: arrayExtension(state.resourceBusy!, action.index!, false), resourceError: arrayExtension(state.resourceError!, action.index!, null), } case "update.error": return { ...state, busy: true, error: null, resourceBusy: arrayExtension(state.resourceBusy!, action.index!, false), resourceError: arrayExtension(state.resourceError!, action.index!, action.value), } case "destroy.start": return { ...state, busy: true, } case "destroy.success": return { ...state, busy: false, resources: arrayExclude(state.resources!, action.index!), resourceBusy: arrayExclude(state.resourceBusy!, action.index!), resourceError: arrayExclude(state.resourceError!, action.index!), } case "destroy.error": return { ...state, busy: false, operationError: action.value, } case "action.start": return { ...state, operationError: null, resourceBusy: arrayExtension(state.resourceBusy!, action.index!, true), } case "action.success": return { ...state, busy: false, resources: arrayExtension(state.resources!, action.index!, action.value), resourceBusy: arrayExtension(state.resourceBusy!, action.index!, false), resourceError: arrayExtension(state.resourceError!, action.index!, null), } case "action.error": return { ...state, busy: true, error: null, resourceBusy: arrayExtension(state.resourceBusy!, action.index!, false), resourceError: arrayExtension(state.resourceError!, action.index!, action.value), } } } export function useManagedViewSet(baseRoute: string, pkKey: keyof Resource, refreshOnMount: boolean = true): ManagedViewSet { const viewset = useViewSet(baseRoute) const [state, dispatch] = useReducer, ManagedReducerAction>>(reducerManagedViewSet, { firstRun: true, busy: false, error: null, operationError: null, resources: null, resourceBusy: null, resourceError: null, }) const refresh: ManagedRefresh = React.useCallback( async () => { if(state.busy) { console.error("Cannot refresh resources while the viewset is busy, ignoring...") return } dispatch({ type: "refresh.start", }) let response: Resource[] try { response = await viewset.list() } catch(err) { dispatch({ type: "refresh.error", value: err }) return } dispatch({ type: "refresh.success", value: response, }) }, [viewset, state, dispatch] ) const create: ManagedCreate = React.useCallback( async data => { if(state.busy) { console.error("Cannot create a new resource while the viewset is busy, ignoring...") return } if(state.error) { console.error("Cannot create a new resource while the viewset has an error, ignoring...") return } dispatch({ type: "create.start", }) let response: Resource try { response = await viewset.create({data}) } catch(err) { dispatch({ type: "create.error", value: err, }) return } dispatch({ type: "create.success", value: response, }) }, [viewset, state, dispatch], ) const command: ManagedCommand = React.useCallback( async (command, data) => { if(state.busy) { console.error("Cannot run a command while the viewset is busy, ignoring...") return } if(state.error) { console.error("Cannot run a command while the viewset has an error, ignoring...") return } dispatch({ type: "command.start", }) let response: Resource[] try { response = await viewset.command({url: `${baseRoute}${command}/`, data}) } catch(err) { dispatch({ type: "command.error", value: err, }) return } dispatch({ type: "command.success", value: response, }) }, [viewset, state, dispatch], ) const update: ManagedUpdate = React.useCallback( async (index, data) => { if(state.busy) { console.error("Cannot update a n resource while the viewset is busy, ignoring...") return } if(state.error) { console.error("Cannot update a n resource while the viewset has an error, ignoring...") return } const request: Resource | undefined = state.resources![index] if(request === undefined) { console.error(`No resource with index ${index}, ignoring...`) return } const pk = request[pkKey] dispatch({ type: "update.start", index: index, }) let response: Resource try { response = await viewset.update(pk, {data}) } catch(err) { dispatch({ type: "update.error", index: index, value: err, }) return } dispatch({ type: "update.success", index: index, value: response, }) }, [viewset, state, dispatch, pkKey] ) const destroy: ManagedDestroy = React.useCallback( async index => { if(state.busy) { console.error("Cannot destroy a resource while the viewset is busy, ignoring...") return } if(state.error) { console.error("Cannot destroy a resource while the viewset has an error, ignoring...") return } const request: Resource | undefined = state.resources![index] if(request === undefined) { console.error(`No resource with index ${index}, ignoring...`) return } const pk = request[pkKey] dispatch({ type: "destroy.start", index: index, }) try { await viewset.destroy(pk) } catch(err) { dispatch({ type: "destroy.error", index: index, }) return } dispatch({ type: "destroy.success", index: index, }) }, [viewset, state, dispatch, pkKey], ) const action: ManagedAction = React.useCallback( async (index, act, data) => { if(state.busy) { console.error("Cannot run an action while the viewset is busy, ignoring...") return } if(state.error) { console.error("Cannot run an action while the viewset has an error, ignoring...") return } const request: Resource | undefined = state.resources![index] if(request === undefined) { console.error(`No resource with index ${index}, ignoring...`) return } const pk = request[pkKey] dispatch({ type: "action.start", index: index, }) let response: Resource try { response = await viewset.action({url: `${baseRoute}${pk}/${action}/`, data}) } catch(err) { dispatch({ type: "action.error", index: index, value: err, }) return } dispatch({ type: "action.success", index: index, value: response, }) }, [viewset, state, dispatch, pkKey], ) const resources: ManagedResource[] | null = useMemo( () => { if(state.resources === null || state.resourceBusy === null || state.resourceError === null) { return null } return state.resources.map( (value, index, __) => { return { value: value, busy: state.resourceBusy![index], error: state.resourceError![index], update: (data) => update(index, data), destroy: () => destroy(index), action: (act, data) => action(index, act, data), } } ) }, [state, update, destroy] ) useEffect( () => { if(!refreshOnMount) return if(!state.firstRun) return if(state.busy) return if(state.error) return if(state.resources) return // noinspection JSIgnoredPromiseFromCall refresh() }, [refresh, state, refreshOnMount] ) return { busy: state.busy, error: state.error, operationError: state.operationError, resources, refresh, create, command, } }