Compare commits

..

2 Commits

6 changed files with 198 additions and 105 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,
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,
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]
}

View File

@ -186,6 +186,13 @@ export const Payment = {
readOnly: true,
columnWidth: 250
},
{
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'invoice',
label: 'Invoice',
@ -195,6 +202,13 @@ export const Payment = {
showHyperlink: true,
columnWidth: 200
},
{
name: 'authorisedAt',
label: 'Authorised At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'payTo',
label: 'Pay To',
@ -211,27 +225,6 @@ export const Payment = {
return objectData?.invoice?.from
}
},
{
name: 'paymentDate',
label: 'Payment Date',
type: 'dateTime',
required: true,
columnWidth: 175
},
{
name: 'postedAt',
label: 'Posted At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'authorisedAt',
label: 'Authorised At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'declinedAt',
label: 'Declined At',
@ -239,13 +232,6 @@ export const Payment = {
readOnly: true,
columnWidth: 175
},
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'amount',
label: 'Amount',
@ -255,19 +241,19 @@ export const Payment = {
required: true,
columnWidth: 150
},
{
name: 'cancelledAt',
label: 'Cancelled At',
type: 'dateTime',
readOnly: true,
columnWidth: 175
},
{
name: 'paymentMethod',
label: 'Payment Method',
type: 'string',
required: false,
columnWidth: 150
},
{
name: 'notes',
label: 'Notes',
type: 'text',
required: false,
columnWidth: 200
}
],
stats: [