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
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
This commit is contained in:
parent
e3a9b8b6f9
commit
51abe4ca67
@ -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,
|
||||||
|
|||||||
@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user