Compare commits
2 Commits
0a347c1396
...
51abe4ca67
| Author | SHA1 | Date | |
|---|---|---|---|
| 51abe4ca67 | |||
| e3a9b8b6f9 |
@ -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,
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -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]
|
||||
}
|
||||
|
||||
@ -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: [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user