diff --git a/src/components/Dashboard/common/ObjectForm.jsx b/src/components/Dashboard/common/ObjectForm.jsx index 9e6c9e89..e8da9b97 100644 --- a/src/components/Dashboard/common/ObjectForm.jsx +++ b/src/components/Dashboard/common/ObjectForm.jsx @@ -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) diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 9803d2e7..b664d07e 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -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} + + + Access Denied + + } + open={showForbiddenModal} + style={{ maxWidth: 430 }} + closable={false} + centered + maskClosable={!forbiddenIsGet} + onCancel={forbiddenIsGet ? handleForbiddenBack : handleForbiddenOk} + footer={[ + + ]} + > + + {forbiddenIsGet + ? 'You do not have permission to view this page.' + : 'You do not have permission to perform this action.'} + + ) }