2021-10-10 15:02:35 +00:00
|
|
|
import * as Axios from "axios-lab"
|
2021-09-29 16:05:55 +00:00
|
|
|
import * as React from "react"
|
2021-10-04 17:41:45 +00:00
|
|
|
import {useEffect, useMemo, useReducer} from "react"
|
2021-10-10 14:01:43 +00:00
|
|
|
import {DjangoResource} from "../types/DjangoTypes"
|
|
|
|
import {arrayExclude, arrayExtension} from "../utils/ArrayExtension"
|
|
|
|
import {useViewSet} from "./useViewSet"
|
2021-09-29 16:05:55 +00:00
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
// Function types
|
2021-09-29 16:05:55 +00:00
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
type ManagedRefresh = () => Promise<void>
|
|
|
|
type ManagedCreate<Resource> = (data: Partial<Resource>) => Promise<void>
|
2021-10-10 15:02:35 +00:00
|
|
|
type ManagedCommand = (method: Axios.Method, cmd: string, data: any) => Promise<void>
|
2021-10-10 14:01:43 +00:00
|
|
|
type ManagedUpdate<Resource> = (index: number, data: Partial<Resource>) => Promise<void>
|
|
|
|
type ManagedDestroy = (index: number) => Promise<void>
|
2021-10-10 15:02:35 +00:00
|
|
|
type ManagedAction = (index: number, method: Axios.Method, act: string, data: any) => Promise<void>
|
2021-09-29 16:05:55 +00:00
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
type ManagedUpdateDetails<Resource> = (data: Partial<Resource>) => Promise<void>
|
|
|
|
type ManagedDestroyDetails = () => Promise<void>
|
2021-10-10 15:02:35 +00:00
|
|
|
type ManagedActionDetails = (method: Axios.Method, act: string, data: any) => Promise<void>
|
2021-09-29 16:05:55 +00:00
|
|
|
|
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
// Public interfaces
|
|
|
|
|
2021-09-29 16:05:55 +00:00
|
|
|
export interface ManagedViewSet<Resource> {
|
|
|
|
busy: boolean,
|
|
|
|
error: Error | null,
|
|
|
|
operationError: Error | null,
|
|
|
|
|
|
|
|
resources: ManagedResource<Resource>[] | null,
|
|
|
|
|
|
|
|
refresh: ManagedRefresh,
|
|
|
|
create: ManagedCreate<Resource>,
|
2021-10-10 14:01:43 +00:00
|
|
|
command: ManagedCommand,
|
2021-09-29 16:05:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export interface ManagedResource<Resource> {
|
|
|
|
value: Resource,
|
|
|
|
busy: boolean,
|
|
|
|
error: Error | null,
|
2021-10-10 14:01:43 +00:00
|
|
|
|
2021-09-29 16:05:55 +00:00
|
|
|
update: ManagedUpdateDetails<Resource>
|
|
|
|
destroy: ManagedDestroyDetails
|
2021-10-10 14:01:43 +00:00
|
|
|
action: ManagedActionDetails,
|
2021-09-29 16:05:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
// Reducer state
|
|
|
|
|
|
|
|
export interface ManagedReducerState<Resource> {
|
2021-09-29 16:05:55 +00:00
|
|
|
firstRun: boolean,
|
|
|
|
|
|
|
|
busy: boolean,
|
|
|
|
error: Error | null,
|
2021-10-10 14:01:43 +00:00
|
|
|
|
2021-09-29 16:05:55 +00:00
|
|
|
operationError: Error | null,
|
|
|
|
|
|
|
|
resources: Resource[] | null,
|
|
|
|
resourceBusy: boolean[] | null,
|
|
|
|
resourceError: (Error | null)[] | null,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
export interface ManagedReducerAction {
|
|
|
|
type: `${"refresh" | "create" | "command" | "update" | "destroy" | "action"}.${"start" | "success" | "error"}`,
|
2021-09-29 16:05:55 +00:00
|
|
|
value?: any,
|
|
|
|
index?: number,
|
|
|
|
}
|
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
|
|
|
|
function reducerManagedViewSet<Resource>(state: ManagedReducerState<Resource>, action: ManagedReducerAction): ManagedReducerState<Resource> {
|
2021-09-29 16:05:55 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2021-09-29 16:05:55 +00:00
|
|
|
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,
|
|
|
|
}
|
2021-10-10 14:01:43 +00:00
|
|
|
|
|
|
|
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),
|
|
|
|
}
|
2021-09-29 16:05:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-10-04 17:41:45 +00:00
|
|
|
export function useManagedViewSet<Resource extends DjangoResource>(baseRoute: string, pkKey: keyof Resource, refreshOnMount: boolean = true): ManagedViewSet<Resource> {
|
2021-09-29 16:05:55 +00:00
|
|
|
const viewset =
|
|
|
|
useViewSet<Resource>(baseRoute)
|
|
|
|
|
|
|
|
const [state, dispatch] =
|
2021-10-10 14:01:43 +00:00
|
|
|
useReducer<React.Reducer<ManagedReducerState<Resource>, ManagedReducerAction>>(reducerManagedViewSet, {
|
2021-09-29 16:05:55 +00:00
|
|
|
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<Resource> =
|
|
|
|
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",
|
2021-10-10 14:01:43 +00:00
|
|
|
value: response,
|
2021-09-29 16:05:55 +00:00
|
|
|
})
|
|
|
|
},
|
2021-10-10 14:01:43 +00:00
|
|
|
[viewset, state, dispatch],
|
|
|
|
)
|
|
|
|
|
|
|
|
const command: ManagedCommand =
|
|
|
|
React.useCallback(
|
2021-10-10 15:02:35 +00:00
|
|
|
async (method, cmd, data) => {
|
2021-10-10 14:01:43 +00:00
|
|
|
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 {
|
2021-10-10 15:02:35 +00:00
|
|
|
response = await viewset.command({url: `${baseRoute}${cmd}/`, data, method})
|
2021-10-10 14:01:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
catch(err) {
|
|
|
|
dispatch({
|
|
|
|
type: "command.error",
|
|
|
|
value: err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: "command.success",
|
|
|
|
value: response,
|
|
|
|
})
|
|
|
|
},
|
2021-10-10 15:02:35 +00:00
|
|
|
[baseRoute, viewset, state, dispatch],
|
2021-09-29 16:05:55 +00:00
|
|
|
)
|
|
|
|
|
2021-10-10 14:01:43 +00:00
|
|
|
|
2021-09-29 16:05:55 +00:00
|
|
|
const update: ManagedUpdate<Resource> =
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
},
|
2021-10-10 14:01:43 +00:00
|
|
|
[viewset, state, dispatch, pkKey],
|
|
|
|
)
|
|
|
|
|
|
|
|
const action: ManagedAction =
|
|
|
|
React.useCallback(
|
2021-10-10 15:02:35 +00:00
|
|
|
async (index, method, act, data) => {
|
2021-10-10 14:01:43 +00:00
|
|
|
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 {
|
2021-10-10 15:02:35 +00:00
|
|
|
response = await viewset.action({url: `${baseRoute}${pk}/${act}/`, data, method})
|
2021-10-10 14:01:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
catch(err) {
|
|
|
|
dispatch({
|
|
|
|
type: "action.error",
|
|
|
|
index: index,
|
|
|
|
value: err,
|
|
|
|
})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
dispatch({
|
|
|
|
type: "action.success",
|
|
|
|
index: index,
|
|
|
|
value: response,
|
|
|
|
})
|
|
|
|
},
|
2021-10-10 15:02:35 +00:00
|
|
|
[viewset, state, dispatch, pkKey, baseRoute],
|
2021-09-29 16:05:55 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const resources: ManagedResource<Resource>[] | 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),
|
2021-10-10 15:02:35 +00:00
|
|
|
action: (method, act, data) => action(index, method, act, data),
|
2021-09-29 16:05:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
},
|
2021-10-10 15:02:35 +00:00
|
|
|
[state, update, destroy, action],
|
2021-09-29 16:05:55 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
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,
|
2021-10-10 14:01:43 +00:00
|
|
|
command,
|
2021-09-29 16:05:55 +00:00
|
|
|
}
|
|
|
|
}
|