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.
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-07-18 14:48:23 +01:00
parent e3a9b8b6f9
commit 51abe4ca67
5 changed files with 177 additions and 70 deletions

View File

@ -27,6 +27,30 @@ const SPOTLIGHT_CACHE_TTL_MS = 10_000
const spotlightCache = new Map() const spotlightCache = new Map()
const runningSpotlightFetches = 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) => { const stableStringify = (value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return `[${value.map(stableStringify).join(',')}]` return `[${value.map(stableStringify).join(',')}]`
@ -77,6 +101,8 @@ const ApiServerProvider = ({ children }) => {
const [showErrorModal, setShowErrorModal] = useState(false) const [showErrorModal, setShowErrorModal] = useState(false)
const [errorModalContent, setErrorModalContent] = useState('') const [errorModalContent, setErrorModalContent] = useState('')
const [retryCallback, setRetryCallback] = useState(null) const [retryCallback, setRetryCallback] = useState(null)
const [userSettings, setUserSettings] = useState(createEmptyUserSettings)
const [userSettingsLoaded, setUserSettingsLoaded] = useState(false)
const subscribedCallbacksRef = useRef(new Map()) const subscribedCallbacksRef = useRef(new Map())
const subscribedLockCallbacksRef = useRef(new Map()) const subscribedLockCallbacksRef = useRef(new Map())
const notificationListenersRef = useRef(new Set()) const notificationListenersRef = useRef(new Set())
@ -129,6 +155,68 @@ const ApiServerProvider = ({ children }) => {
notificationListenersRef.current.delete(callback) 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(() => { const connectToServer = useCallback(() => {
if (token && authenticated == true) { if (token && authenticated == true) {
logger.debug('Token is available, connecting to api server...') logger.debug('Token is available, connecting to api server...')
@ -143,7 +231,20 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('connect', () => { newSocket.on('connect', () => {
logger.debug('Api Server connected') 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) setConnecting(false)
setConnected(true) setConnected(true)
setError(null) setError(null)
@ -180,6 +281,8 @@ const ApiServerProvider = ({ children }) => {
setError('Api Server disconnected') setError('Api Server disconnected')
clearSubscriptions() clearSubscriptions()
setConnected(false) setConnected(false)
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(false)
}) })
newSocket.on('connect_error', (err) => { newSocket.on('connect_error', (err) => {
@ -188,6 +291,8 @@ const ApiServerProvider = ({ children }) => {
setError('Api Server connection error') setError('Api Server connection error')
clearSubscriptions() clearSubscriptions()
setConnected(false) setConnected(false)
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(false)
}) })
newSocket.on('error', (err) => { newSocket.on('error', (err) => {
@ -197,7 +302,14 @@ const ApiServerProvider = ({ children }) => {
socketRef.current = newSocket socketRef.current = newSocket
} }
}, [token, authenticated, messageApi, handleLockUpdate, clearSubscriptions]) }, [
token,
authenticated,
messageApi,
handleLockUpdate,
clearSubscriptions,
getUserSettings
])
useEffect(() => { useEffect(() => {
if (token && authenticated == true) { if (token && authenticated == true) {
@ -206,6 +318,8 @@ const ApiServerProvider = ({ children }) => {
logger.debug('Token not available, disconnecting api server...') logger.debug('Token not available, disconnecting api server...')
socketRef.current.disconnect() socketRef.current.disconnect()
socketRef.current = null socketRef.current = null
setUserSettings(createEmptyUserSettings())
setUserSettingsLoaded(false)
} }
// Clean up function // Clean up function
@ -1831,6 +1945,10 @@ const ApiServerProvider = ({ children }) => {
error, error,
connecting, connecting,
connected, connected,
userSettings,
userSettingsLoaded,
getUserSettings,
updateUserSettings,
lockObject, lockObject,
unlockObject, unlockObject,
fetchObjectLock, fetchObjectLock,

View File

@ -1,29 +1,20 @@
import { useState, useEffect } from 'react' import { useCallback, useContext } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
const useCollapseState = (componentName, defaultState = {}) => { const useCollapseState = (componentName, defaultState = {}) => {
const getInitialState = () => { const { userSettings, updateUserSettings } = useContext(ApiServerContext)
const stored = sessionStorage.getItem(`${componentName}_collapseState`) const collapseState =
if (stored) { userSettings.collapseState[componentName] ?? defaultState
return JSON.parse(stored)
}
return defaultState
}
const [collapseState, setCollapseState] = useState(getInitialState) const updateCollapseState = useCallback(
(key, value) => {
useEffect(() => { updateUserSettings('collapseState', componentName, {
sessionStorage.setItem( ...collapseState,
`${componentName}_collapseState`, [key]: value
JSON.stringify(collapseState) })
) },
}, [collapseState, componentName]) [collapseState, componentName, updateUserSettings]
)
const updateCollapseState = (key, value) => {
setCollapseState((prev) => ({
...prev,
[key]: value
}))
}
return [collapseState, updateCollapseState] return [collapseState, updateCollapseState]
} }

View File

@ -1,13 +1,10 @@
import { useState, useEffect } from 'react' import { useCallback, useContext, useMemo } from 'react'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import { ApiServerContext } from '../context/ApiServerContext'
const useColumnVisibility = (type, customColumns) => { const useColumnVisibility = (type, customColumns) => {
const getInitialVisibility = () => { const { userSettings, updateUserSettings } = useContext(ApiServerContext)
const storageKey = `${type}_columnVisibility` const defaultVisibility = useMemo(() => {
const stored = sessionStorage.getItem(storageKey)
if (stored) {
return JSON.parse(stored)
}
// Default visibility - all columns visible // Default visibility - all columns visible
if (customColumns && Array.isArray(customColumns)) { if (customColumns && Array.isArray(customColumns)) {
return customColumns.reduce((acc, col) => { return customColumns.reduce((acc, col) => {
@ -28,23 +25,20 @@ const useColumnVisibility = (type, customColumns) => {
} }
return acc return acc
}, {}) }, {})
} }, [customColumns, type])
const [columnVisibility, setColumnVisibility] = useState(getInitialVisibility) const columnVisibility =
userSettings.columnVisibility[type] ?? defaultVisibility
useEffect(() => { const updateColumnVisibility = useCallback(
sessionStorage.setItem( (key, value) => {
`${type}_columnVisibility`, updateUserSettings('columnVisibility', type, {
JSON.stringify(columnVisibility) ...columnVisibility,
) [key]: value
}, [columnVisibility, type]) })
},
const updateColumnVisibility = (key, value) => { [columnVisibility, type, updateUserSettings]
setColumnVisibility((prev) => ({ )
...prev,
[key]: value
}))
}
return [columnVisibility, updateColumnVisibility] return [columnVisibility, updateColumnVisibility]
} }

View File

@ -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 useFilterSidebarVisibility = (componentName, defaultValue = false) => {
const getInitialVisibility = () => { const { userSettings, updateUserSettings } = useContext(ApiServerContext)
const stored = sessionStorage.getItem(`${componentName}_filterSidebarVisibility`) const showFilterSidebar =
if (stored !== null) { userSettings.filterSidebarVisibility[componentName] ?? defaultValue
return stored === 'true'
}
return defaultValue
}
const [showFilterSidebar, setShowFilterSidebar] = useState(getInitialVisibility) const setShowFilterSidebar = useCallback(
(nextValue) => {
useEffect(() => { const value =
sessionStorage.setItem(`${componentName}_filterSidebarVisibility`, showFilterSidebar) typeof nextValue === 'function'
}, [showFilterSidebar, componentName]) ? nextValue(showFilterSidebar)
: nextValue
updateUserSettings('filterSidebarVisibility', componentName, value)
},
[componentName, showFilterSidebar, updateUserSettings]
)
return [showFilterSidebar, setShowFilterSidebar] return [showFilterSidebar, setShowFilterSidebar]
} }

View File

@ -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 useViewMode = (componentName, defaultMode = 'list') => {
const getInitialViewMode = () => { const { userSettings, updateUserSettings } = useContext(ApiServerContext)
const stored = sessionStorage.getItem(`${componentName}_viewMode`) const viewMode = userSettings.viewMode[componentName] ?? defaultMode
return stored ? stored : defaultMode
}
const [viewMode, setViewMode] = useState(getInitialViewMode) const setViewMode = useCallback(
(nextMode) => {
useEffect(() => { const mode =
sessionStorage.setItem(`${componentName}_viewMode`, viewMode) typeof nextMode === 'function' ? nextMode(viewMode) : nextMode
}, [viewMode, componentName]) updateUserSettings('viewMode', componentName, mode)
},
[componentName, updateUserSettings, viewMode]
)
return [viewMode, setViewMode] return [viewMode, setViewMode]
} }