diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx index 0768c18..82b21cd 100644 --- a/src/components/Dashboard/context/ApiServerContext.jsx +++ b/src/components/Dashboard/context/ApiServerContext.jsx @@ -27,6 +27,30 @@ const SPOTLIGHT_CACHE_TTL_MS = 10_000 const spotlightCache = new Map() const runningSpotlightFetches = new Map() +const createEmptyUserSettings = () => ({ + viewMode: {}, + filterSidebarVisibility: {}, + columnVisibility: {}, + collapseState: {} +}) + +const normalizeUserSettingsCategory = (category) => + category && typeof category === 'object' && !Array.isArray(category) + ? category + : {} + +const normalizeUserSettings = (settings = {}) => ({ + viewMode: normalizeUserSettingsCategory(settings?.viewMode), + filterSidebarVisibility: normalizeUserSettingsCategory( + settings?.filterSidebarVisibility + ), + columnVisibility: normalizeUserSettingsCategory(settings?.columnVisibility), + collapseState: normalizeUserSettingsCategory(settings?.collapseState) +}) + +const emitWithAcknowledgement = (socket, eventName, data) => + new Promise((resolve) => socket.emit(eventName, data, resolve)) + const stableStringify = (value) => { if (Array.isArray(value)) { return `[${value.map(stableStringify).join(',')}]` @@ -77,6 +101,8 @@ const ApiServerProvider = ({ children }) => { const [showErrorModal, setShowErrorModal] = useState(false) const [errorModalContent, setErrorModalContent] = useState('') const [retryCallback, setRetryCallback] = useState(null) + const [userSettings, setUserSettings] = useState(createEmptyUserSettings) + const [userSettingsLoaded, setUserSettingsLoaded] = useState(false) const subscribedCallbacksRef = useRef(new Map()) const subscribedLockCallbacksRef = useRef(new Map()) const notificationListenersRef = useRef(new Set()) @@ -129,6 +155,68 @@ const ApiServerProvider = ({ children }) => { notificationListenersRef.current.delete(callback) }, []) + const getUserSettings = useCallback(async (socket = socketRef.current) => { + if (!socket?.connected) { + return null + } + + try { + const result = await emitWithAcknowledgement( + socket, + 'getUserSettings', + {} + ) + if (!result?.success) { + throw new Error(result?.error || 'Unable to get user settings') + } + + const settings = normalizeUserSettings(result.settings) + setUserSettings(settings) + return settings + } catch (err) { + logger.error('Failed to get user settings:', err) + setUserSettings(createEmptyUserSettings()) + return null + } finally { + setUserSettingsLoaded(true) + } + }, []) + + const updateUserSettings = useCallback( + async (category, key, value) => { + setUserSettings((currentSettings) => ({ + ...currentSettings, + [category]: { + ...currentSettings[category], + [key]: value + } + })) + + const socket = socketRef.current + if (!socket?.connected) { + return null + } + + try { + const result = await emitWithAcknowledgement( + socket, + 'updateUserSettings', + { category, key, value } + ) + if (!result?.success) { + throw new Error(result?.error || 'Unable to update user settings') + } + return result.settings + } catch (err) { + logger.error('Failed to update user settings:', err) + messageApi.error('Failed to save user settings') + await getUserSettings(socket) + return null + } + }, + [getUserSettings, messageApi] + ) + const connectToServer = useCallback(() => { if (token && authenticated == true) { logger.debug('Token is available, connecting to api server...') @@ -143,7 +231,20 @@ const ApiServerProvider = ({ children }) => { newSocket.on('connect', () => { logger.debug('Api Server connected') - newSocket.emit('authenticate', { token: token }, () => { + newSocket.emit('authenticate', { token: token }, async (result) => { + if (result?.valid !== true) { + setConnecting(false) + setError('Api Server authentication failed') + newSocket.disconnect() + return + } + + setUserSettings(createEmptyUserSettings()) + setUserSettingsLoaded(false) + await getUserSettings(newSocket) + if (!newSocket.connected) { + return + } setConnecting(false) setConnected(true) setError(null) @@ -180,6 +281,8 @@ const ApiServerProvider = ({ children }) => { setError('Api Server disconnected') clearSubscriptions() setConnected(false) + setUserSettings(createEmptyUserSettings()) + setUserSettingsLoaded(false) }) newSocket.on('connect_error', (err) => { @@ -188,6 +291,8 @@ const ApiServerProvider = ({ children }) => { setError('Api Server connection error') clearSubscriptions() setConnected(false) + setUserSettings(createEmptyUserSettings()) + setUserSettingsLoaded(false) }) newSocket.on('error', (err) => { @@ -197,7 +302,14 @@ const ApiServerProvider = ({ children }) => { socketRef.current = newSocket } - }, [token, authenticated, messageApi, handleLockUpdate, clearSubscriptions]) + }, [ + token, + authenticated, + messageApi, + handleLockUpdate, + clearSubscriptions, + getUserSettings + ]) useEffect(() => { if (token && authenticated == true) { @@ -206,6 +318,8 @@ const ApiServerProvider = ({ children }) => { logger.debug('Token not available, disconnecting api server...') socketRef.current.disconnect() socketRef.current = null + setUserSettings(createEmptyUserSettings()) + setUserSettingsLoaded(false) } // Clean up function @@ -1831,6 +1945,10 @@ const ApiServerProvider = ({ children }) => { error, connecting, connected, + userSettings, + userSettingsLoaded, + getUserSettings, + updateUserSettings, lockObject, unlockObject, fetchObjectLock, diff --git a/src/components/Dashboard/hooks/useCollapseState.jsx b/src/components/Dashboard/hooks/useCollapseState.jsx index 2c0e06e..054dc7e 100644 --- a/src/components/Dashboard/hooks/useCollapseState.jsx +++ b/src/components/Dashboard/hooks/useCollapseState.jsx @@ -1,29 +1,20 @@ -import { useState, useEffect } from 'react' +import { useCallback, useContext } from 'react' +import { ApiServerContext } from '../context/ApiServerContext' const useCollapseState = (componentName, defaultState = {}) => { - const getInitialState = () => { - const stored = sessionStorage.getItem(`${componentName}_collapseState`) - if (stored) { - return JSON.parse(stored) - } - return defaultState - } + const { userSettings, updateUserSettings } = useContext(ApiServerContext) + const collapseState = + userSettings.collapseState[componentName] ?? defaultState - const [collapseState, setCollapseState] = useState(getInitialState) - - useEffect(() => { - sessionStorage.setItem( - `${componentName}_collapseState`, - JSON.stringify(collapseState) - ) - }, [collapseState, componentName]) - - const updateCollapseState = (key, value) => { - setCollapseState((prev) => ({ - ...prev, - [key]: value - })) - } + const updateCollapseState = useCallback( + (key, value) => { + updateUserSettings('collapseState', componentName, { + ...collapseState, + [key]: value + }) + }, + [collapseState, componentName, updateUserSettings] + ) return [collapseState, updateCollapseState] } diff --git a/src/components/Dashboard/hooks/useColumnVisibility.jsx b/src/components/Dashboard/hooks/useColumnVisibility.jsx index 7c4048c..e425b51 100644 --- a/src/components/Dashboard/hooks/useColumnVisibility.jsx +++ b/src/components/Dashboard/hooks/useColumnVisibility.jsx @@ -1,13 +1,10 @@ -import { useState, useEffect } from 'react' +import { useCallback, useContext, useMemo } from 'react' import { getModelByName } from '../../../database/ObjectModels' +import { ApiServerContext } from '../context/ApiServerContext' const useColumnVisibility = (type, customColumns) => { - const getInitialVisibility = () => { - const storageKey = `${type}_columnVisibility` - const stored = sessionStorage.getItem(storageKey) - if (stored) { - return JSON.parse(stored) - } + const { userSettings, updateUserSettings } = useContext(ApiServerContext) + const defaultVisibility = useMemo(() => { // Default visibility - all columns visible if (customColumns && Array.isArray(customColumns)) { return customColumns.reduce((acc, col) => { @@ -28,23 +25,20 @@ const useColumnVisibility = (type, customColumns) => { } return acc }, {}) - } + }, [customColumns, type]) - const [columnVisibility, setColumnVisibility] = useState(getInitialVisibility) + const columnVisibility = + userSettings.columnVisibility[type] ?? defaultVisibility - useEffect(() => { - sessionStorage.setItem( - `${type}_columnVisibility`, - JSON.stringify(columnVisibility) - ) - }, [columnVisibility, type]) - - const updateColumnVisibility = (key, value) => { - setColumnVisibility((prev) => ({ - ...prev, - [key]: value - })) - } + const updateColumnVisibility = useCallback( + (key, value) => { + updateUserSettings('columnVisibility', type, { + ...columnVisibility, + [key]: value + }) + }, + [columnVisibility, type, updateUserSettings] + ) return [columnVisibility, updateColumnVisibility] } diff --git a/src/components/Dashboard/hooks/useFilterSidebarVisibility.jsx b/src/components/Dashboard/hooks/useFilterSidebarVisibility.jsx index 598c83e..66920d1 100644 --- a/src/components/Dashboard/hooks/useFilterSidebarVisibility.jsx +++ b/src/components/Dashboard/hooks/useFilterSidebarVisibility.jsx @@ -1,19 +1,21 @@ -import { useState, useEffect } from 'react' +import { useCallback, useContext } from 'react' +import { ApiServerContext } from '../context/ApiServerContext' const useFilterSidebarVisibility = (componentName, defaultValue = false) => { - const getInitialVisibility = () => { - const stored = sessionStorage.getItem(`${componentName}_filterSidebarVisibility`) - if (stored !== null) { - return stored === 'true' - } - return defaultValue - } + const { userSettings, updateUserSettings } = useContext(ApiServerContext) + const showFilterSidebar = + userSettings.filterSidebarVisibility[componentName] ?? defaultValue - const [showFilterSidebar, setShowFilterSidebar] = useState(getInitialVisibility) - - useEffect(() => { - sessionStorage.setItem(`${componentName}_filterSidebarVisibility`, showFilterSidebar) - }, [showFilterSidebar, componentName]) + const setShowFilterSidebar = useCallback( + (nextValue) => { + const value = + typeof nextValue === 'function' + ? nextValue(showFilterSidebar) + : nextValue + updateUserSettings('filterSidebarVisibility', componentName, value) + }, + [componentName, showFilterSidebar, updateUserSettings] + ) return [showFilterSidebar, setShowFilterSidebar] } diff --git a/src/components/Dashboard/hooks/useViewMode.jsx b/src/components/Dashboard/hooks/useViewMode.jsx index cec1e39..514b019 100644 --- a/src/components/Dashboard/hooks/useViewMode.jsx +++ b/src/components/Dashboard/hooks/useViewMode.jsx @@ -1,16 +1,18 @@ -import { useState, useEffect } from 'react' +import { useCallback, useContext } from 'react' +import { ApiServerContext } from '../context/ApiServerContext' const useViewMode = (componentName, defaultMode = 'list') => { - const getInitialViewMode = () => { - const stored = sessionStorage.getItem(`${componentName}_viewMode`) - return stored ? stored : defaultMode - } + const { userSettings, updateUserSettings } = useContext(ApiServerContext) + const viewMode = userSettings.viewMode[componentName] ?? defaultMode - const [viewMode, setViewMode] = useState(getInitialViewMode) - - useEffect(() => { - sessionStorage.setItem(`${componentName}_viewMode`, viewMode) - }, [viewMode, componentName]) + const setViewMode = useCallback( + (nextMode) => { + const mode = + typeof nextMode === 'function' ? nextMode(viewMode) : nextMode + updateUserSettings('viewMode', componentName, mode) + }, + [componentName, updateUserSettings, viewMode] + ) return [viewMode, setViewMode] }