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 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,

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 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]
}

View File

@ -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]
}

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 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]
}

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 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]
}