Compare commits

..

No commits in common. "51abe4ca679eedfca49c80df8cf6d355c8940a26" and "0a347c1396bd99178479289df378e6276032ea61" have entirely different histories.

6 changed files with 105 additions and 198 deletions

View File

@ -27,30 +27,6 @@ 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(',')}]`
@ -101,8 +77,6 @@ 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())
@ -155,68 +129,6 @@ 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...')
@ -231,20 +143,7 @@ const ApiServerProvider = ({ children }) => {
newSocket.on('connect', () => { newSocket.on('connect', () => {
logger.debug('Api Server connected') logger.debug('Api Server connected')
newSocket.emit('authenticate', { token: token }, async (result) => { newSocket.emit('authenticate', { token: token }, () => {
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)
@ -281,8 +180,6 @@ 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) => {
@ -291,8 +188,6 @@ 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) => {
@ -302,14 +197,7 @@ 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) {
@ -318,8 +206,6 @@ 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
@ -1945,10 +1831,6 @@ const ApiServerProvider = ({ children }) => {
error, error,
connecting, connecting,
connected, connected,
userSettings,
userSettingsLoaded,
getUserSettings,
updateUserSettings,
lockObject, lockObject,
unlockObject, unlockObject,
fetchObjectLock, fetchObjectLock,

View File

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

View File

@ -1,10 +1,13 @@
import { useCallback, useContext, useMemo } from 'react' import { useState, useEffect } 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 { userSettings, updateUserSettings } = useContext(ApiServerContext) const getInitialVisibility = () => {
const defaultVisibility = useMemo(() => { const storageKey = `${type}_columnVisibility`
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) => {
@ -25,20 +28,23 @@ const useColumnVisibility = (type, customColumns) => {
} }
return acc return acc
}, {}) }, {})
}, [customColumns, type]) }
const columnVisibility = const [columnVisibility, setColumnVisibility] = useState(getInitialVisibility)
userSettings.columnVisibility[type] ?? defaultVisibility
const updateColumnVisibility = useCallback( useEffect(() => {
(key, value) => { sessionStorage.setItem(
updateUserSettings('columnVisibility', type, { `${type}_columnVisibility`,
...columnVisibility, JSON.stringify(columnVisibility)
[key]: value )
}) }, [columnVisibility, type])
},
[columnVisibility, type, updateUserSettings] const updateColumnVisibility = (key, value) => {
) setColumnVisibility((prev) => ({
...prev,
[key]: value
}))
}
return [columnVisibility, updateColumnVisibility] return [columnVisibility, updateColumnVisibility]
} }

View File

@ -1,21 +1,19 @@
import { useCallback, useContext } from 'react' import { useState, useEffect } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
const useFilterSidebarVisibility = (componentName, defaultValue = false) => { const useFilterSidebarVisibility = (componentName, defaultValue = false) => {
const { userSettings, updateUserSettings } = useContext(ApiServerContext) const getInitialVisibility = () => {
const showFilterSidebar = const stored = sessionStorage.getItem(`${componentName}_filterSidebarVisibility`)
userSettings.filterSidebarVisibility[componentName] ?? defaultValue if (stored !== null) {
return stored === 'true'
}
return defaultValue
}
const setShowFilterSidebar = useCallback( const [showFilterSidebar, setShowFilterSidebar] = useState(getInitialVisibility)
(nextValue) => {
const value = useEffect(() => {
typeof nextValue === 'function' sessionStorage.setItem(`${componentName}_filterSidebarVisibility`, showFilterSidebar)
? nextValue(showFilterSidebar) }, [showFilterSidebar, componentName])
: nextValue
updateUserSettings('filterSidebarVisibility', componentName, value)
},
[componentName, showFilterSidebar, updateUserSettings]
)
return [showFilterSidebar, setShowFilterSidebar] return [showFilterSidebar, setShowFilterSidebar]
} }

View File

@ -1,18 +1,16 @@
import { useCallback, useContext } from 'react' import { useState, useEffect } from 'react'
import { ApiServerContext } from '../context/ApiServerContext'
const useViewMode = (componentName, defaultMode = 'list') => { const useViewMode = (componentName, defaultMode = 'list') => {
const { userSettings, updateUserSettings } = useContext(ApiServerContext) const getInitialViewMode = () => {
const viewMode = userSettings.viewMode[componentName] ?? defaultMode const stored = sessionStorage.getItem(`${componentName}_viewMode`)
return stored ? stored : defaultMode
}
const setViewMode = useCallback( const [viewMode, setViewMode] = useState(getInitialViewMode)
(nextMode) => {
const mode = useEffect(() => {
typeof nextMode === 'function' ? nextMode(viewMode) : nextMode sessionStorage.setItem(`${componentName}_viewMode`, viewMode)
updateUserSettings('viewMode', componentName, mode) }, [viewMode, componentName])
},
[componentName, updateUserSettings, viewMode]
)
return [viewMode, setViewMode] return [viewMode, setViewMode]
} }

View File

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