From 52fd0ebd634e4ada8fa2696e04d5dfc657495d59 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 7 Mar 2026 20:32:02 +0000 Subject: [PATCH] Enhance FilePreview and FileUpload components with error handling and minimal display option. Refactor AuthContext to improve user info retrieval and session management. --- .../Dashboard/common/FilePreview.jsx | 15 +++++ .../Dashboard/common/FileUpload.jsx | 14 ++++- .../Dashboard/common/ObjectProperty.jsx | 4 ++ .../Dashboard/common/PropertyChanges.jsx | 2 + .../Dashboard/context/AuthContext.jsx | 62 ++++++++++++++----- 5 files changed, 82 insertions(+), 15 deletions(-) diff --git a/src/components/Dashboard/common/FilePreview.jsx b/src/components/Dashboard/common/FilePreview.jsx index 0091b66..14fcdac 100644 --- a/src/components/Dashboard/common/FilePreview.jsx +++ b/src/components/Dashboard/common/FilePreview.jsx @@ -12,10 +12,21 @@ const FilePreview = ({ file, style = {} }) => { const [fileObjectUrl, setFileObjectUrl] = useState(null) const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) const fetchPreview = useCallback(async () => { + if (error != null) { + return + } setLoading(true) + console.log('fetching file content', file) const objectUrl = await fetchFileContent(file, false) + if (objectUrl == null) { + setLoading(false) + console.error('Failed to fetch file content', file) + setError('Failed to fetch file content') + return + } setFileObjectUrl(objectUrl) setLoading(false) }, [file, fetchFileContent]) @@ -30,6 +41,10 @@ const FilePreview = ({ file, style = {} }) => { return } + if (error != null) { + return
{error}
+ } + const isGcode = ['.g', '.gcode'].includes( (file?.extension || '').toLowerCase() ) diff --git a/src/components/Dashboard/common/FileUpload.jsx b/src/components/Dashboard/common/FileUpload.jsx index e4ff122..be83fe1 100644 --- a/src/components/Dashboard/common/FileUpload.jsx +++ b/src/components/Dashboard/common/FileUpload.jsx @@ -7,12 +7,14 @@ import ObjectSelect from './ObjectSelect' import FileList from './FileList' import PlusIcon from '../../Icons/PlusIcon' import { LoadingOutlined } from '@ant-design/icons' +import FileIcon from '../../Icons/FileIcon' const { Text } = Typography const FileUpload = ({ value, onChange, + minimal = false, multiple = true, defaultPreviewOpen = false, showPreview = true, @@ -96,6 +98,15 @@ const FileUpload = ({ } } + if (minimal == true) { + return ( + + + {currentFiles[0]?.name} + + ) + } + return ( {hasNoItems && uploading == false ? ( @@ -170,7 +181,8 @@ FileUpload.propTypes = { multiple: PropTypes.bool, showPreview: PropTypes.bool, showInfo: PropTypes.bool, - defaultPreviewOpen: PropTypes.bool + defaultPreviewOpen: PropTypes.bool, + minimal: PropTypes.bool } export default FileUpload diff --git a/src/components/Dashboard/common/ObjectProperty.jsx b/src/components/Dashboard/common/ObjectProperty.jsx index 8a7992b..1bca065 100644 --- a/src/components/Dashboard/common/ObjectProperty.jsx +++ b/src/components/Dashboard/common/ObjectProperty.jsx @@ -559,6 +559,7 @@ const ObjectProperty = ({ return ( { {...changeProperty} longId={false} minimal={true} + showPreview={false} objectData={value?.old} maxWidth='200px' /> @@ -71,6 +72,7 @@ const PropertyChanges = ({ type, value }) => { {...changeProperty} longId={false} minimal={true} + showPreview={false} objectData={value?.new} maxWidth='200px' /> diff --git a/src/components/Dashboard/context/AuthContext.jsx b/src/components/Dashboard/context/AuthContext.jsx index 2510980..c4c796d 100644 --- a/src/components/Dashboard/context/AuthContext.jsx +++ b/src/components/Dashboard/context/AuthContext.jsx @@ -42,6 +42,19 @@ const AuthContext = createContext() const { Text } = Typography +const extractUserFromAuthData = (authData) => { + if (!authData || typeof authData !== 'object') return null + if (authData.user && typeof authData.user === 'object') return authData.user + + // Some endpoints may return the "user" fields at the top-level. Only treat it + // as a user object if it looks like one (avoid confusing token-only payloads). + const looksLikeUser = + authData._id || authData.username || authData.email || authData.name + if (looksLikeUser) return authData + + return null +} + const AuthProvider = ({ children }) => { const [messageApi, contextHolder] = message.useMessage() const [notificationApi, notificationContextHolder] = @@ -80,19 +93,6 @@ const AuthProvider = ({ children }) => { redirectType = 'app-scheme' } - const extractUserFromAuthData = (authData) => { - if (!authData || typeof authData !== 'object') return null - if (authData.user && typeof authData.user === 'object') return authData.user - - // Some endpoints may return the "user" fields at the top-level. Only treat it - // as a user object if it looks like one (avoid confusing token-only payloads). - const looksLikeUser = - authData._id || authData.username || authData.email || authData.name - if (looksLikeUser) return authData - - return null - } - const isSessionExpired = (session) => { if (!session?.expiresAt) return true const now = new Date() @@ -100,6 +100,32 @@ const AuthProvider = ({ children }) => { return expirationDate <= now } + const getUserInfo = useCallback( + async (token, getIsCancelled = () => false) => { + try { + const response = await axios.get(`${config.backendUrl}/auth/user`, { + headers: { Authorization: `Bearer ${token}` } + }) + if (!getIsCancelled() && response.status === 200 && response.data) { + const nextUser = extractUserFromAuthData(response.data) + if (nextUser) setUserProfile(nextUser) + } + } catch (err) { + if (!getIsCancelled()) { + logger.debug('Failed to refresh user from API:', err) + if (err.response?.status === 401) { + setAuthenticated(false) + setToken(null) + setUserProfile(null) + setExpiresAt(null) + setShowUnauthorizedModal(true) + } + } + } + }, + [] + ) + const persistSession = useCallback( async ({ token: nextToken, expiresAt: nextExpiresAt, user: nextUser }) => { if (isElectron) { @@ -154,6 +180,10 @@ const AuthProvider = ({ children }) => { setUserProfile(session.user) setExpiresAt(session.expiresAt) setAuthenticated(true) + + if (session.user) { + getUserInfo(session.token, () => cancelled) + } } else if (!cancelled) { setAuthenticated(false) setUserProfile(null) @@ -173,6 +203,10 @@ const AuthProvider = ({ children }) => { setUserProfile(storedUser) setExpiresAt(storedExpiresAt) setAuthenticated(true) + + if (storedToken && storedUser) { + getUserInfo(storedToken, () => cancelled) + } } } else if (!cancelled) { setAuthenticated(false) @@ -197,7 +231,7 @@ const AuthProvider = ({ children }) => { return () => { cancelled = true } - }, [isElectron, getAuthSession, clearPersistedSession]) + }, [isElectron, getAuthSession, clearPersistedSession, getUserInfo]) // Set up cookie synchronization between tabs useEffect(() => {