Compare commits

..

No commits in common. "749ddf0c315514d3f80b932499cd748590a58dc4" and "56479ea358e0413a1338c72910dd2892a08dc335" have entirely different histories.

5 changed files with 54 additions and 216 deletions

View File

@ -5,7 +5,6 @@ import { useMessageContext } from '../context/MessageContext'
import PropTypes from 'prop-types'
import set from 'lodash/set'
import { getModelByName } from '../../../database/ObjectModels'
import {
mergeFormData,
stripNestedObjectProperties,
@ -15,34 +14,26 @@ import {
const buildObjectFromEntries = (entries = []) => {
return entries.reduce((acc, entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) {
return acc
}
set(acc, namePath, value)
return acc
}, {})
}
// Patch computed fields onto the complete object.
// This mirrors the handling used by ObjectForm.
const applyComputedEntries = (base, entries = []) => {
const result = mergeFormData(base || {})
entries.forEach((entry) => {
const { namePath, value } = entry || {}
if (!Array.isArray(namePath) || value === undefined) return
set(result, namePath, value)
})
return result
}
/**
* NewObjectForm is a reusable form component for creating new objects.
*
* It handles form validation, submission, and error handling logic.
*
* Props:
@ -54,33 +45,21 @@ const applyComputedEntries = (base, entries = []) => {
* }) => ReactNode
*/
const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
// Do not initialise this from defaultValues.
// The form initialisation effect below is the single source of truth,
// matching ObjectForm's fetched-object handling.
const [objectData, setObjectData] = useState({
...defaultValues,
_isEditing: true
})
const [submitLoading, setSubmitLoading] = useState(false)
const [formValid, setFormValid] = useState(false)
const [form] = Form.useForm()
const validationRunRef = useRef(0)
const formUpdateValues = Form.useWatch([], form)
const { showSuccess, showError: showMessageError } = useMessageContext()
const { createObject, showError } = useContext(ApiServerContext)
const model = getModelByName(type)
const validateForm = useCallback(() => {
const validationRun = ++validationRunRef.current
let cancelled = false
const timeoutId = setTimeout(() => {
form
.validateFields({ validateOnly: true })
@ -102,59 +81,27 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
}
}, [form])
const model = getModelByName(type)
const calculateComputedValues = useCallback(
(currentData, modelDefinition, options = {}) => {
return calculateModelComputedEntries(
currentData,
modelDefinition,
options
)
return calculateModelComputedEntries(currentData, modelDefinition, options)
},
[]
)
/*
* Initialise the form from defaultValues.
*
* This deliberately follows ObjectForm's initial fetch handling:
*
* defaultValues
* ↓
* calculate computed values
* ↓
* apply computed values to the complete object
* ↓
* setObjectData(...)
* ↓
* form.setFieldsValue(...)
*
* Keeping the exact same object structure in objectData and the Form
* is important for object-select fields.
*/
// Set initial form values when defaultValues change
useEffect(() => {
const initialData = mergeFormData(defaultValues || {})
const computedEntries = calculateComputedValues(initialData, model)
const initialFormData = applyComputedEntries(initialData, computedEntries)
const nextObjectData = {
...initialFormData,
_isEditing: true
if (Object.keys(defaultValues).length > 0) {
const computedEntries = calculateComputedValues(defaultValues, model)
const initialFormData = applyComputedEntries(
defaultValues,
computedEntries
)
form.setFieldsValue(initialFormData)
setObjectData((prev) => mergeFormData(prev, initialFormData))
return validateForm()
}
// Clear any previous values before applying the new defaults.
// This is particularly important when defaultValues changes while
// the component remains mounted.
form.resetFields()
// Use the complete object as the form value, just like ObjectForm.
form.setFieldsValue(initialFormData)
// Keep objectData in sync with exactly the same initial data.
setObjectData(nextObjectData)
return validateForm()
}, [form, defaultValues, calculateComputedValues, model, validateForm])
// Validate form on change
@ -165,34 +112,23 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const handleSubmit = async () => {
try {
setSubmitLoading(true)
const currentFormValues = form.getFieldsValue()
const currentFormData = mergeFormData(objectData || {}, currentFormValues)
const computedEntries = calculateComputedValues(currentFormData, model)
const computedObjectData = applyComputedEntries(
currentFormData,
computedEntries
)
const payload = stripNestedObjectProperties(computedObjectData, model)
const newObject = await createObject(type, payload)
showSuccess('Object created successfully')
return newObject
} catch (err) {
console.error(err)
if (err.errorFields) {
return
}
showMessageError('Failed to create object')
showError(
`Failed to create object. Message: ${err.message}. Code: ${err.code}`,
() => handleSubmit()
@ -210,17 +146,12 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
disabled={submitLoading}
onValuesChange={(_changedValues, allFormValues) => {
const currentFormData = mergeFormData(objectData || {}, allFormValues)
const computedEntries = calculateComputedValues(currentFormData, model)
if (Array.isArray(computedEntries) && computedEntries.length > 0) {
computedEntries.forEach(({ namePath, value }) => {
if (!Array.isArray(namePath) || value === undefined) {
return
}
if (!Array.isArray(namePath) || value === undefined) return
const currentValue = form.getFieldValue(namePath)
if (currentValue !== value) {
if (typeof form.setFieldValue === 'function') {
form.setFieldValue(namePath, value)
@ -228,7 +159,6 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
const fallbackPayload = buildObjectFromEntries([
{ namePath, value }
])
form.setFieldsValue(fallbackPayload)
}
}
@ -236,9 +166,6 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
}
const allValues = applyComputedEntries(allFormValues, computedEntries)
allValues._isEditing = true
setObjectData((prev) => mergeFormData(prev, allValues))
}}
>
@ -258,11 +185,8 @@ const NewObjectForm = ({ type, style, defaultValues = {}, children }) => {
NewObjectForm.propTypes = {
type: PropTypes.string.isRequired,
children: PropTypes.func.isRequired,
style: PropTypes.object,
defaultValues: PropTypes.object
}

View File

@ -25,14 +25,6 @@ const areValuesEqual = (v1, v2) => {
return String(id1) === String(id2)
}
const toSelectValue = (id) => {
if (id == null || id === '') return null
return String(id).toLowerCase()
}
const getValueId = (item) =>
item && typeof item === 'object' ? item._id : item
const getFirstSelectableLeaf = (nodes) => {
if (!Array.isArray(nodes)) return null
for (const node of nodes) {
@ -77,23 +69,6 @@ const isValueInTree = (nodes, id) => {
return findTreeNodeByValue(nodes, id) != null
}
const getSelectValueFromExternal = (externalValue, multiple, nodes) => {
if (externalValue == null || !Array.isArray(nodes) || nodes.length === 0) {
return undefined
}
if (multiple) {
const values = Array.isArray(externalValue) ? externalValue : []
const ids = values
.map((item) => toSelectValue(getValueId(item)))
.filter((id) => id != null && isValueInTree(nodes, id))
return ids
}
const node = findTreeNodeByValue(nodes, getValueId(externalValue))
return node?.value
}
const isExternalValueMissing = (
externalValue,
multiple,
@ -553,27 +528,9 @@ const ObjectSelect = ({
setObjectList(objects)
setTreeData(treeNodes)
treeDataRef.current = treeNodes
const syncedValue = getSelectValueFromExternal(
valueRef.current,
multiple,
treeNodes
)
if (multiple) {
if (Array.isArray(syncedValue) && syncedValue.length > 0) {
setTreeSelectValue(syncedValue)
setValueNotFound(false)
clearedMissingValueRef.current = false
}
} else if (syncedValue != null) {
setTreeSelectValue(syncedValue)
setValueNotFound(false)
clearedMissingValueRef.current = false
}
return { treeNodes, objects }
},
[buildTreeData, multiple]
[buildTreeData]
)
const buildFilterFromNode = useCallback(
@ -676,17 +633,6 @@ const ObjectSelect = ({
const onTreeSelectChange = useCallback(
(value) => {
const isEmptySelection = multiple
? !Array.isArray(value) || value.length === 0
: value == null || value === ''
if (
isEmptySelection &&
treeDataRef.current.length === 0 &&
valueRef.current != null
) {
return
}
setValueNotFound(false)
clearedMissingValueRef.current = false
// Mark this as an internal change
@ -881,7 +827,7 @@ const ObjectSelect = ({
setExpandedKeys([...new Set(pathKeys)])
setTreeSelectValue(
value
.map((item) => toSelectValue(getValueId(item)))
.map((item) => (item && typeof item === 'object' ? item._id : item))
.filter((id) => id != null)
)
setInitialized(true)
@ -934,7 +880,7 @@ const ObjectSelect = ({
setExpandedKeys(pathKeys)
// Fetch with the new filter
handleFetchObjectsProperties(valueFilter)
setTreeSelectValue(toSelectValue(valueRef.current._id))
setTreeSelectValue(valueRef.current._id)
setInitialized(true)
return
}
@ -959,13 +905,7 @@ const ObjectSelect = ({
setInitialized(true)
}
}
const timeoutId = setTimeout(() => {
handleValue()
}, 10)
return () => {
clearTimeout(timeoutId)
}
handleValue()
}, [
value,
filter,
@ -1000,7 +940,6 @@ const ObjectSelect = ({
setTreeVersion((v) => v + 1)
setExpandedKeys([])
setInitialized(false)
valueRef.current = null
onTreeSelectChange(null)
setTreeSelectValue(null)
setInitialLoading(true)
@ -1024,12 +963,7 @@ const ObjectSelect = ({
if (changeSource == 'external') {
setObjectPropertiesTree({})
setTreeData([])
treeDataRef.current = []
setInitialized(false)
setInitialLoading(true)
valueRef.current = null
clearedMissingValueRef.current = false
setValueNotFound(false)
prevValuesRef.current = { type, masterFilter }
}

View File

@ -12,14 +12,13 @@ import { message, Modal, Space, Button, Typography, Flex } from 'antd'
import PropTypes from 'prop-types'
import { AuthContext } from './AuthContext'
import { useLocation, useNavigate } from 'react-router-dom'
import { LoadingOutlined } from '@ant-design/icons'
import axios from 'axios'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import ReloadIcon from '../../Icons/ReloadIcon'
import LockIcon from '../../Icons/LockIcon'
import config from '../../../config'
import loglevel from 'loglevel'
import { getModelByName } from '../../../database/ObjectModels'
import ProgressDisplay from '../common/ProgressDisplay'
const logger = loglevel.getLogger('ApiServerContext')
@ -2921,59 +2920,42 @@ const ApiServerProvider = ({ children }) => {
{children}
<Modal
title={
!isReconnecting ? (
<Space size={'middle'}>
<ExclamationOctagonIcon />
Connection Lost
</Space>
) : (
false
)
<Space size={'middle'}>
<ExclamationOctagonIcon />
Connection Lost
</Space>
}
open={Boolean(token) && authenticated == true && connectionIssue}
style={{ maxWidth: !isReconnecting ? 480 : 260 }}
style={{ maxWidth: 480 }}
zIndex={3000}
closable={false}
height={isReconnecting ? 20 : undefined}
centered
className={isReconnecting ? 'loading-modal' : undefined}
maskClosable={false}
getContainer={() => document.body}
footer={
!isReconnecting
? [
<Button
key='reconnect'
loading={isReconnecting}
onClick={() => attemptReconnect()}
>
Reconnect
</Button>
]
: false
}
footer={[
<Button
key='reconnect'
loading={isReconnecting}
onClick={() => attemptReconnect()}
>
Reconnect
</Button>
]}
>
{!isReconnecting ? (
<Flex vertical gap='middle'>
<Text>
{isReconnecting
? 'Reconnecting to the API server...'
: `Lost connection to the API server. Reconnecting in ${reconnectSecondsRemaining} second${
reconnectSecondsRemaining === 1 ? '' : 's'
}...`}
</Text>
<ProgressDisplay
percent={isReconnecting ? 0 : 100 - reconnectProgress}
showInfo={false}
status={'exception'}
/>
</Flex>
) : (
<Space size={'middle'}>
<LoadingOutlined />
<Text style={{ margin: 0 }}>Reconnecting, please wait...</Text>
</Space>
)}
<Flex vertical gap='middle'>
<Text>
{isReconnecting
? 'Reconnecting to the API server...'
: `Lost connection to the API server. Reconnecting in ${reconnectSecondsRemaining} second${
reconnectSecondsRemaining === 1 ? '' : 's'
}...`}
</Text>
<ProgressDisplay
percent={isReconnecting ? 0 : 100 - reconnectProgress}
showInfo={false}
status={'exception'}
/>
</Flex>
</Modal>
<Modal
title={

View File

@ -128,7 +128,7 @@ export const SalesOrder = {
return objectData?.state?.type != 'draft'
},
objectData: (objectData) => ({
order: objectData,
order: { _id: objectData._id },
orderType: 'salesOrder',
syncAmount: 'itemPrice'
})
@ -142,12 +142,10 @@ export const SalesOrder = {
disabled: (objectData) => {
return objectData?.state?.type != 'draft'
},
objectData: (objectData) => {
return {
orderType: 'salesOrder',
order: objectData
}
}
objectData: (objectData) => ({
orderType: 'salesOrder',
order: { _id: objectData._id }
})
},
{
name: 'newInvoice',

View File

@ -74,7 +74,7 @@ export const User = {
disabled: (objectData) => {
return objectData?._user?._id != objectData?._id
},
objectData: (objectData) => ({ user: objectData?._user || objectData })
objectData: (objectData) => ({ user: objectData })
}
],
pages: [