From 51abe4ca679eedfca49c80df8cf6d355c8940a26 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Sat, 18 Jul 2026 14:48:23 +0100 Subject: [PATCH] Implement user settings management in ApiServerContext, including functions to get and update user settings. Refactor hooks for collapse state, column visibility, filter sidebar visibility, and view mode to utilize centralized user settings, enhancing state management and improving user experience. --- .../Dashboard/context/ApiServerContext.jsx | 122 +++++++++++++++++- .../Dashboard/hooks/useCollapseState.jsx | 37 ++---- .../Dashboard/hooks/useColumnVisibility.jsx | 38 +++--- .../hooks/useFilterSidebarVisibility.jsx | 28 ++-- .../Dashboard/hooks/useViewMode.jsx | 22 ++-- 5 files changed, 177 insertions(+), 70 deletions(-) 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] }