Refactor ObjectForm and ApiServerContext for improved error handling and user feedback
- Introduced a new utility function, capitalizeFirstLetter, to standardize label formatting in ObjectForm. - Enhanced error handling in ObjectForm by providing more descriptive error messages related to editing and fetching objects. - Updated ApiServerContext to handle forbidden access scenarios with a dedicated modal, improving user experience during permission errors. - Refactored state management in ObjectForm to streamline loading and editing states, ensuring better performance and clarity. - Improved overall code readability and structure in both components.
This commit is contained in:
parent
76d00a8be1
commit
69d515efd9
@ -89,13 +89,20 @@ const getEditDisabled = (model, objectData, userProfile) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (typeof editAction.disabled === 'function') {
|
if (typeof editAction.disabled === 'function') {
|
||||||
return (
|
return editAction.disabled({ ...objectData, _user: userProfile }) ?? false
|
||||||
editAction.disabled({ ...objectData, _user: userProfile }) ?? false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return !!editAction.disabled
|
return !!editAction.disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const capitalizeFirstLetter = (value) => {
|
||||||
|
return String(value).charAt(0).toUpperCase() + String(value).slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const getModelMessageLabel = (model) => {
|
||||||
|
const modelObject = getModelByName(model)
|
||||||
|
return capitalizeFirstLetter(modelObject.label.toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ObjectForm is a reusable form component for editing any object type.
|
* ObjectForm is a reusable form component for editing any object type.
|
||||||
* It handles fetching, updating, locking, unlocking, and validation logic.
|
* It handles fetching, updating, locking, unlocking, and validation logic.
|
||||||
@ -110,9 +117,18 @@ const getEditDisabled = (model, objectData, userProfile) => {
|
|||||||
* - setCurrentObject: boolean (sync object data to ActionsContext)
|
* - setCurrentObject: boolean (sync object data to ActionsContext)
|
||||||
* - onStateChange: receives form state including editDisabled (from model edit action)
|
* - onStateChange: receives form state including editDisabled (from model edit action)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const ObjectForm = forwardRef(
|
const ObjectForm = forwardRef(
|
||||||
(
|
(
|
||||||
{ id, type, style, children, onEdit, onStateChange, setCurrentObject = false },
|
{
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
style,
|
||||||
|
children,
|
||||||
|
onEdit,
|
||||||
|
onStateChange,
|
||||||
|
setCurrentObject = false
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const [objectData, setObjectData] = useState(null)
|
const [objectData, setObjectData] = useState(null)
|
||||||
@ -127,7 +143,7 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const formUpdateValues = Form.useWatch([], form)
|
const formUpdateValues = Form.useWatch([], form)
|
||||||
const { showSuccess, showError: showMessageError } = useMessageContext()
|
const { showSuccess, showError } = useMessageContext()
|
||||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false)
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
const {
|
const {
|
||||||
@ -137,7 +153,6 @@ const ObjectForm = forwardRef(
|
|||||||
setObjectActivity,
|
setObjectActivity,
|
||||||
clearObjectActivity,
|
clearObjectActivity,
|
||||||
fetchObjectActivities,
|
fetchObjectActivities,
|
||||||
showError,
|
|
||||||
connected,
|
connected,
|
||||||
subscribeToObjectUpdates,
|
subscribeToObjectUpdates,
|
||||||
subscribeToObjectActivity,
|
subscribeToObjectActivity,
|
||||||
@ -381,7 +396,9 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
releaseEditingState()
|
releaseEditingState()
|
||||||
clearAction()
|
clearAction()
|
||||||
showMessageError('Another user is already editing this item')
|
showError(
|
||||||
|
`Another user is already editing this ${getModelMessageLabel(type).toLowerCase()}.`
|
||||||
|
)
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
id,
|
id,
|
||||||
@ -390,7 +407,7 @@ const ObjectForm = forwardRef(
|
|||||||
setObjectActivity,
|
setObjectActivity,
|
||||||
releaseEditingState,
|
releaseEditingState,
|
||||||
clearAction,
|
clearAction,
|
||||||
showMessageError
|
showError
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -411,7 +428,11 @@ const ObjectForm = forwardRef(
|
|||||||
onStateChangeRef.current({
|
onStateChangeRef.current({
|
||||||
formValid: true,
|
formValid: true,
|
||||||
objectData: mergedObjectData,
|
objectData: mergedObjectData,
|
||||||
editDisabled: getEditDisabled(model, mergedObjectData, userProfile)
|
editDisabled: getEditDisabled(
|
||||||
|
model,
|
||||||
|
mergedObjectData,
|
||||||
|
userProfile
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
@ -419,7 +440,11 @@ const ObjectForm = forwardRef(
|
|||||||
onStateChangeRef.current({
|
onStateChangeRef.current({
|
||||||
formValid: false,
|
formValid: false,
|
||||||
objectData: mergedObjectData,
|
objectData: mergedObjectData,
|
||||||
editDisabled: getEditDisabled(model, mergedObjectData, userProfile)
|
editDisabled: getEditDisabled(
|
||||||
|
model,
|
||||||
|
mergedObjectData,
|
||||||
|
userProfile
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}, 150)
|
}, 150)
|
||||||
@ -486,10 +511,8 @@ const ObjectForm = forwardRef(
|
|||||||
onStateChangeRef.current({ loading: false })
|
onStateChangeRef.current({ loading: false })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
showMessageError('Failed to fetch object info')
|
|
||||||
showError(
|
showError(
|
||||||
`Failed to fetch object information. Message: ${err.message}. Code: ${err.code}`,
|
`Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.`
|
||||||
fetchObject
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
@ -498,7 +521,6 @@ const ObjectForm = forwardRef(
|
|||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
form,
|
form,
|
||||||
showMessageError,
|
|
||||||
showError,
|
showError,
|
||||||
calculateComputedValues,
|
calculateComputedValues,
|
||||||
model,
|
model,
|
||||||
@ -662,14 +684,18 @@ const ObjectForm = forwardRef(
|
|||||||
|
|
||||||
if (getBeingEditedByOther(latestActivities, userProfile?._id)) {
|
if (getBeingEditedByOther(latestActivities, userProfile?._id)) {
|
||||||
clearAction()
|
clearAction()
|
||||||
showMessageError('Another user is already editing this item')
|
showError(
|
||||||
|
`Another user is already editing this ${getModelMessageLabel(type).toLowerCase()}.`
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const activityResult = await setObjectActivity(id, type, 'editing')
|
const activityResult = await setObjectActivity(id, type, 'editing')
|
||||||
if (activityResult?.success === false) {
|
if (activityResult?.success === false) {
|
||||||
clearAction()
|
clearAction()
|
||||||
showMessageError('Another user is already editing this item')
|
showError(
|
||||||
|
`Another user is already editing this ${getModelMessageLabel(type).toLowerCase()}.`
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,7 +721,9 @@ const ObjectForm = forwardRef(
|
|||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
showMessageError('Failed to start editing')
|
showError(
|
||||||
|
`Failed to start editing ${getModelMessageLabel(type).toLowerCase()}.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -705,8 +733,12 @@ const ObjectForm = forwardRef(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
try {
|
let error
|
||||||
const value = await form.validateFields()
|
const value = await form.validateFields().catch((err) => {
|
||||||
|
error = err
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
setEditLoading(true)
|
setEditLoading(true)
|
||||||
|
|
||||||
const currentFormData = {
|
const currentFormData = {
|
||||||
@ -714,32 +746,37 @@ const ObjectForm = forwardRef(
|
|||||||
...value
|
...value
|
||||||
}
|
}
|
||||||
onStateChangeRef.current({ editLoading: true })
|
onStateChangeRef.current({ editLoading: true })
|
||||||
await updateObject(id, type, currentFormData)
|
const updatedObject = await updateObject(id, type, currentFormData)
|
||||||
setIsEditing(false)
|
|
||||||
isEditingRef.current = false
|
if (updatedObject.error) {
|
||||||
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
console.log('THERE IS AN ERROR')
|
||||||
setObjectData({
|
error = updatedObject
|
||||||
...objectData,
|
} else {
|
||||||
...currentFormData,
|
setIsEditing(false)
|
||||||
_isEditing: isEditingRef.current
|
isEditingRef.current = false
|
||||||
})
|
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
||||||
setObjectActivity(id, type, 'viewing')
|
|
||||||
showSuccess('Information updated successfully')
|
setObjectData({
|
||||||
} catch (err) {
|
...objectData,
|
||||||
console.error(err)
|
...currentFormData,
|
||||||
if (err.errorFields) {
|
...updatedObject,
|
||||||
return
|
_isEditing: isEditingRef.current
|
||||||
|
})
|
||||||
|
setObjectActivity(id, type, 'viewing')
|
||||||
|
showSuccess(`${getModelMessageLabel(type)} edited successfully!`)
|
||||||
}
|
}
|
||||||
showMessageError('Failed to update information')
|
|
||||||
showError(
|
|
||||||
`Failed to update information. Message: ${err.message}. Code: ${err.code}`,
|
|
||||||
() => handleUpdate()
|
|
||||||
)
|
|
||||||
} finally {
|
|
||||||
handleFetchObject()
|
|
||||||
setEditLoading(false)
|
|
||||||
onStateChangeRef.current({ editLoading: false })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.log('ERROR', error)
|
||||||
|
showError(
|
||||||
|
`Failed to update ${getModelMessageLabel(type).toLowerCase()} information.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFetchObject()
|
||||||
|
setEditLoading(false)
|
||||||
|
onStateChangeRef.current({ editLoading: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
@ -751,15 +788,13 @@ const ObjectForm = forwardRef(
|
|||||||
try {
|
try {
|
||||||
await deleteObject(id, type)
|
await deleteObject(id, type)
|
||||||
setDeleteModalOpen(false)
|
setDeleteModalOpen(false)
|
||||||
showSuccess('Deleted successfully')
|
showSuccess(`${getModelMessageLabel(type)} deleted successfully!`)
|
||||||
navigate(-2)
|
navigate(-2)
|
||||||
// Optionally: trigger a callback to parent to remove this object from view
|
// Optionally: trigger a callback to parent to remove this object from view
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
showMessageError('Failed to delete')
|
|
||||||
showError(
|
showError(
|
||||||
`Failed to delete. Message: ${err.message}. Code: ${err.code}`,
|
`Failed to delete ${getModelMessageLabel(type).toLowerCase()}.`
|
||||||
confirmDelete
|
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
setDeleteLoading(false)
|
setDeleteLoading(false)
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import {
|
|||||||
useCallback
|
useCallback
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import io from 'socket.io-client'
|
import io from 'socket.io-client'
|
||||||
import { message, Modal, Space, Button } from 'antd'
|
import { message, Modal, Space, Button, Typography } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { AuthContext } from './AuthContext'
|
import { AuthContext } from './AuthContext'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
@ -16,12 +16,15 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||||
|
import LockIcon from '../../Icons/LockIcon'
|
||||||
import config from '../../../config'
|
import config from '../../../config'
|
||||||
import loglevel from 'loglevel'
|
import loglevel from 'loglevel'
|
||||||
import { getModelByName } from '../../../database/ObjectModels'
|
import { getModelByName } from '../../../database/ObjectModels'
|
||||||
const logger = loglevel.getLogger('ApiServerContext')
|
const logger = loglevel.getLogger('ApiServerContext')
|
||||||
logger.setLevel(config.logLevel)
|
logger.setLevel(config.logLevel)
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
const SPOTLIGHT_CACHE_TTL_MS = 10_000
|
const SPOTLIGHT_CACHE_TTL_MS = 10_000
|
||||||
|
|
||||||
const spotlightCache = new Map()
|
const spotlightCache = new Map()
|
||||||
@ -105,6 +108,8 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
const [showErrorModal, setShowErrorModal] = useState(false)
|
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||||
const [errorModalContent, setErrorModalContent] = useState('')
|
const [errorModalContent, setErrorModalContent] = useState('')
|
||||||
const [retryCallback, setRetryCallback] = useState(null)
|
const [retryCallback, setRetryCallback] = useState(null)
|
||||||
|
const [showForbiddenModal, setShowForbiddenModal] = useState(false)
|
||||||
|
const [forbiddenIsGet, setForbiddenIsGet] = useState(false)
|
||||||
const [userSettings, setUserSettings] = useState(createEmptyUserSettings)
|
const [userSettings, setUserSettings] = useState(createEmptyUserSettings)
|
||||||
const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
|
const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
|
||||||
const subscribedCallbacksRef = useRef(new Map())
|
const subscribedCallbacksRef = useRef(new Map())
|
||||||
@ -1030,6 +1035,19 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
setUnauthenticated()
|
setUnauthenticated()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (code == 'FORBIDDEN') {
|
||||||
|
const method = (
|
||||||
|
error.config?.method ||
|
||||||
|
error.response?.config?.method ||
|
||||||
|
error.response?.data?.method ||
|
||||||
|
''
|
||||||
|
)
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
setForbiddenIsGet(method === 'get')
|
||||||
|
setShowForbiddenModal(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
var content = `Error ${error.code} (${error.status}): ${error.message}`
|
var content = `Error ${error.code} (${error.status}): ${error.message}`
|
||||||
if (error.response?.data?.error) {
|
if (error.response?.data?.error) {
|
||||||
content = `${error.response?.data?.error} (${error.status})`
|
content = `${error.response?.data?.error} (${error.status})`
|
||||||
@ -1050,6 +1068,15 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
setRetryCallback(null)
|
setRetryCallback(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleForbiddenOk = () => {
|
||||||
|
setShowForbiddenModal(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleForbiddenBack = () => {
|
||||||
|
setShowForbiddenModal(false)
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
|
|
||||||
// Generalized fetchObject function
|
// Generalized fetchObject function
|
||||||
const fetchObject = async (id, type) => {
|
const fetchObject = async (id, type) => {
|
||||||
const fetchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
const fetchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
||||||
@ -1282,10 +1309,10 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
return response.data
|
return response.data
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
setError(err, () => {
|
showError(err, () => {
|
||||||
updateObject(id, type, value)
|
updateObject(id, type, value)
|
||||||
})
|
})
|
||||||
return {}
|
return { error: err.response.data.error }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2447,6 +2474,35 @@ const ApiServerProvider = ({ children }) => {
|
|||||||
>
|
>
|
||||||
{errorModalContent}
|
{errorModalContent}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Space size={'middle'}>
|
||||||
|
<LockIcon />
|
||||||
|
Access Denied
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
open={showForbiddenModal}
|
||||||
|
style={{ maxWidth: 430 }}
|
||||||
|
closable={false}
|
||||||
|
centered
|
||||||
|
maskClosable={!forbiddenIsGet}
|
||||||
|
onCancel={forbiddenIsGet ? handleForbiddenBack : handleForbiddenOk}
|
||||||
|
footer={[
|
||||||
|
<Button
|
||||||
|
key={forbiddenIsGet ? 'back' : 'ok'}
|
||||||
|
type='default'
|
||||||
|
onClick={forbiddenIsGet ? handleForbiddenBack : handleForbiddenOk}
|
||||||
|
>
|
||||||
|
{forbiddenIsGet ? 'Back' : 'OK'}
|
||||||
|
</Button>
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
{forbiddenIsGet
|
||||||
|
? 'You do not have permission to view this page.'
|
||||||
|
: 'You do not have permission to perform this action.'}
|
||||||
|
</Text>
|
||||||
|
</Modal>
|
||||||
</ApiServerContext.Provider>
|
</ApiServerContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user