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
|
||||
}
|
||||
if (typeof editAction.disabled === 'function') {
|
||||
return (
|
||||
editAction.disabled({ ...objectData, _user: userProfile }) ?? false
|
||||
)
|
||||
return editAction.disabled({ ...objectData, _user: userProfile }) ?? false
|
||||
}
|
||||
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.
|
||||
* 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)
|
||||
* - onStateChange: receives form state including editDisabled (from model edit action)
|
||||
*/
|
||||
|
||||
const ObjectForm = forwardRef(
|
||||
(
|
||||
{ id, type, style, children, onEdit, onStateChange, setCurrentObject = false },
|
||||
{
|
||||
id,
|
||||
type,
|
||||
style,
|
||||
children,
|
||||
onEdit,
|
||||
onStateChange,
|
||||
setCurrentObject = false
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const [objectData, setObjectData] = useState(null)
|
||||
@ -127,7 +143,7 @@ const ObjectForm = forwardRef(
|
||||
|
||||
const [form] = Form.useForm()
|
||||
const formUpdateValues = Form.useWatch([], form)
|
||||
const { showSuccess, showError: showMessageError } = useMessageContext()
|
||||
const { showSuccess, showError } = useMessageContext()
|
||||
const [deleteModalOpen, setDeleteModalOpen] = useState(false)
|
||||
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||
const {
|
||||
@ -137,7 +153,6 @@ const ObjectForm = forwardRef(
|
||||
setObjectActivity,
|
||||
clearObjectActivity,
|
||||
fetchObjectActivities,
|
||||
showError,
|
||||
connected,
|
||||
subscribeToObjectUpdates,
|
||||
subscribeToObjectActivity,
|
||||
@ -381,7 +396,9 @@ const ObjectForm = forwardRef(
|
||||
|
||||
releaseEditingState()
|
||||
clearAction()
|
||||
showMessageError('Another user is already editing this item')
|
||||
showError(
|
||||
`Another user is already editing this ${getModelMessageLabel(type).toLowerCase()}.`
|
||||
)
|
||||
},
|
||||
[
|
||||
id,
|
||||
@ -390,7 +407,7 @@ const ObjectForm = forwardRef(
|
||||
setObjectActivity,
|
||||
releaseEditingState,
|
||||
clearAction,
|
||||
showMessageError
|
||||
showError
|
||||
]
|
||||
)
|
||||
|
||||
@ -411,7 +428,11 @@ const ObjectForm = forwardRef(
|
||||
onStateChangeRef.current({
|
||||
formValid: true,
|
||||
objectData: mergedObjectData,
|
||||
editDisabled: getEditDisabled(model, mergedObjectData, userProfile)
|
||||
editDisabled: getEditDisabled(
|
||||
model,
|
||||
mergedObjectData,
|
||||
userProfile
|
||||
)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
@ -419,7 +440,11 @@ const ObjectForm = forwardRef(
|
||||
onStateChangeRef.current({
|
||||
formValid: false,
|
||||
objectData: mergedObjectData,
|
||||
editDisabled: getEditDisabled(model, mergedObjectData, userProfile)
|
||||
editDisabled: getEditDisabled(
|
||||
model,
|
||||
mergedObjectData,
|
||||
userProfile
|
||||
)
|
||||
})
|
||||
})
|
||||
}, 150)
|
||||
@ -486,10 +511,8 @@ const ObjectForm = forwardRef(
|
||||
onStateChangeRef.current({ loading: false })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showMessageError('Failed to fetch object info')
|
||||
showError(
|
||||
`Failed to fetch object information. Message: ${err.message}. Code: ${err.code}`,
|
||||
fetchObject
|
||||
`Failed to fetch ${getModelMessageLabel(type).toLowerCase()} info.`
|
||||
)
|
||||
}
|
||||
}, [
|
||||
@ -498,7 +521,6 @@ const ObjectForm = forwardRef(
|
||||
id,
|
||||
type,
|
||||
form,
|
||||
showMessageError,
|
||||
showError,
|
||||
calculateComputedValues,
|
||||
model,
|
||||
@ -662,14 +684,18 @@ const ObjectForm = forwardRef(
|
||||
|
||||
if (getBeingEditedByOther(latestActivities, userProfile?._id)) {
|
||||
clearAction()
|
||||
showMessageError('Another user is already editing this item')
|
||||
showError(
|
||||
`Another user is already editing this ${getModelMessageLabel(type).toLowerCase()}.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const activityResult = await setObjectActivity(id, type, 'editing')
|
||||
if (activityResult?.success === false) {
|
||||
clearAction()
|
||||
showMessageError('Another user is already editing this item')
|
||||
showError(
|
||||
`Another user is already editing this ${getModelMessageLabel(type).toLowerCase()}.`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@ -695,7 +721,9 @@ const ObjectForm = forwardRef(
|
||||
})
|
||||
} catch (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 () => {
|
||||
try {
|
||||
const value = await form.validateFields()
|
||||
let error
|
||||
const value = await form.validateFields().catch((err) => {
|
||||
error = err
|
||||
})
|
||||
|
||||
if (!error) {
|
||||
setEditLoading(true)
|
||||
|
||||
const currentFormData = {
|
||||
@ -714,32 +746,37 @@ const ObjectForm = forwardRef(
|
||||
...value
|
||||
}
|
||||
onStateChangeRef.current({ editLoading: true })
|
||||
await updateObject(id, type, currentFormData)
|
||||
setIsEditing(false)
|
||||
isEditingRef.current = false
|
||||
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
||||
setObjectData({
|
||||
...objectData,
|
||||
...currentFormData,
|
||||
_isEditing: isEditingRef.current
|
||||
})
|
||||
setObjectActivity(id, type, 'viewing')
|
||||
showSuccess('Information updated successfully')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (err.errorFields) {
|
||||
return
|
||||
const updatedObject = await updateObject(id, type, currentFormData)
|
||||
|
||||
if (updatedObject.error) {
|
||||
console.log('THERE IS AN ERROR')
|
||||
error = updatedObject
|
||||
} else {
|
||||
setIsEditing(false)
|
||||
isEditingRef.current = false
|
||||
onStateChangeRef.current({ isEditing: isEditingRef.current })
|
||||
|
||||
setObjectData({
|
||||
...objectData,
|
||||
...currentFormData,
|
||||
...updatedObject,
|
||||
_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 = () => {
|
||||
@ -751,15 +788,13 @@ const ObjectForm = forwardRef(
|
||||
try {
|
||||
await deleteObject(id, type)
|
||||
setDeleteModalOpen(false)
|
||||
showSuccess('Deleted successfully')
|
||||
showSuccess(`${getModelMessageLabel(type)} deleted successfully!`)
|
||||
navigate(-2)
|
||||
// Optionally: trigger a callback to parent to remove this object from view
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
showMessageError('Failed to delete')
|
||||
showError(
|
||||
`Failed to delete. Message: ${err.message}. Code: ${err.code}`,
|
||||
confirmDelete
|
||||
`Failed to delete ${getModelMessageLabel(type).toLowerCase()}.`
|
||||
)
|
||||
} finally {
|
||||
setDeleteLoading(false)
|
||||
|
||||
@ -8,7 +8,7 @@ import {
|
||||
useCallback
|
||||
} from 'react'
|
||||
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 { AuthContext } from './AuthContext'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
@ -16,12 +16,15 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import axios from 'axios'
|
||||
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
|
||||
import ReloadIcon from '../../Icons/ReloadIcon'
|
||||
import LockIcon from '../../Icons/LockIcon'
|
||||
import config from '../../../config'
|
||||
import loglevel from 'loglevel'
|
||||
import { getModelByName } from '../../../database/ObjectModels'
|
||||
const logger = loglevel.getLogger('ApiServerContext')
|
||||
logger.setLevel(config.logLevel)
|
||||
|
||||
const { Text } = Typography
|
||||
|
||||
const SPOTLIGHT_CACHE_TTL_MS = 10_000
|
||||
|
||||
const spotlightCache = new Map()
|
||||
@ -105,6 +108,8 @@ const ApiServerProvider = ({ children }) => {
|
||||
const [showErrorModal, setShowErrorModal] = useState(false)
|
||||
const [errorModalContent, setErrorModalContent] = useState('')
|
||||
const [retryCallback, setRetryCallback] = useState(null)
|
||||
const [showForbiddenModal, setShowForbiddenModal] = useState(false)
|
||||
const [forbiddenIsGet, setForbiddenIsGet] = useState(false)
|
||||
const [userSettings, setUserSettings] = useState(createEmptyUserSettings)
|
||||
const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
|
||||
const subscribedCallbacksRef = useRef(new Map())
|
||||
@ -1030,6 +1035,19 @@ const ApiServerProvider = ({ children }) => {
|
||||
setUnauthenticated()
|
||||
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}`
|
||||
if (error.response?.data?.error) {
|
||||
content = `${error.response?.data?.error} (${error.status})`
|
||||
@ -1050,6 +1068,15 @@ const ApiServerProvider = ({ children }) => {
|
||||
setRetryCallback(null)
|
||||
}
|
||||
|
||||
const handleForbiddenOk = () => {
|
||||
setShowForbiddenModal(false)
|
||||
}
|
||||
|
||||
const handleForbiddenBack = () => {
|
||||
setShowForbiddenModal(false)
|
||||
navigate(-1)
|
||||
}
|
||||
|
||||
// Generalized fetchObject function
|
||||
const fetchObject = async (id, type) => {
|
||||
const fetchUrl = `${config.backendUrl}/${getObjectEndpoint(type)}/${id}`
|
||||
@ -1282,10 +1309,10 @@ const ApiServerProvider = ({ children }) => {
|
||||
return response.data
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError(err, () => {
|
||||
showError(err, () => {
|
||||
updateObject(id, type, value)
|
||||
})
|
||||
return {}
|
||||
return { error: err.response.data.error }
|
||||
}
|
||||
}
|
||||
|
||||
@ -2447,6 +2474,35 @@ const ApiServerProvider = ({ children }) => {
|
||||
>
|
||||
{errorModalContent}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user