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

View File

@ -25,14 +25,6 @@ const areValuesEqual = (v1, v2) => {
return String(id1) === String(id2) 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) => { const getFirstSelectableLeaf = (nodes) => {
if (!Array.isArray(nodes)) return null if (!Array.isArray(nodes)) return null
for (const node of nodes) { for (const node of nodes) {
@ -77,23 +69,6 @@ const isValueInTree = (nodes, id) => {
return findTreeNodeByValue(nodes, id) != null 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 = ( const isExternalValueMissing = (
externalValue, externalValue,
multiple, multiple,
@ -553,27 +528,9 @@ const ObjectSelect = ({
setObjectList(objects) setObjectList(objects)
setTreeData(treeNodes) setTreeData(treeNodes)
treeDataRef.current = 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 } return { treeNodes, objects }
}, },
[buildTreeData, multiple] [buildTreeData]
) )
const buildFilterFromNode = useCallback( const buildFilterFromNode = useCallback(
@ -676,17 +633,6 @@ const ObjectSelect = ({
const onTreeSelectChange = useCallback( const onTreeSelectChange = useCallback(
(value) => { (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) setValueNotFound(false)
clearedMissingValueRef.current = false clearedMissingValueRef.current = false
// Mark this as an internal change // Mark this as an internal change
@ -881,7 +827,7 @@ const ObjectSelect = ({
setExpandedKeys([...new Set(pathKeys)]) setExpandedKeys([...new Set(pathKeys)])
setTreeSelectValue( setTreeSelectValue(
value value
.map((item) => toSelectValue(getValueId(item))) .map((item) => (item && typeof item === 'object' ? item._id : item))
.filter((id) => id != null) .filter((id) => id != null)
) )
setInitialized(true) setInitialized(true)
@ -934,7 +880,7 @@ const ObjectSelect = ({
setExpandedKeys(pathKeys) setExpandedKeys(pathKeys)
// Fetch with the new filter // Fetch with the new filter
handleFetchObjectsProperties(valueFilter) handleFetchObjectsProperties(valueFilter)
setTreeSelectValue(toSelectValue(valueRef.current._id)) setTreeSelectValue(valueRef.current._id)
setInitialized(true) setInitialized(true)
return return
} }
@ -959,13 +905,7 @@ const ObjectSelect = ({
setInitialized(true) setInitialized(true)
} }
} }
const timeoutId = setTimeout(() => {
handleValue() handleValue()
}, 10)
return () => {
clearTimeout(timeoutId)
}
}, [ }, [
value, value,
filter, filter,
@ -1000,7 +940,6 @@ const ObjectSelect = ({
setTreeVersion((v) => v + 1) setTreeVersion((v) => v + 1)
setExpandedKeys([]) setExpandedKeys([])
setInitialized(false) setInitialized(false)
valueRef.current = null
onTreeSelectChange(null) onTreeSelectChange(null)
setTreeSelectValue(null) setTreeSelectValue(null)
setInitialLoading(true) setInitialLoading(true)
@ -1024,12 +963,7 @@ const ObjectSelect = ({
if (changeSource == 'external') { if (changeSource == 'external') {
setObjectPropertiesTree({}) setObjectPropertiesTree({})
setTreeData([]) setTreeData([])
treeDataRef.current = []
setInitialized(false) setInitialized(false)
setInitialLoading(true)
valueRef.current = null
clearedMissingValueRef.current = false
setValueNotFound(false)
prevValuesRef.current = { type, masterFilter } 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 PropTypes from 'prop-types'
import { AuthContext } from './AuthContext' import { AuthContext } from './AuthContext'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { LoadingOutlined } from '@ant-design/icons'
import axios from 'axios' import axios from 'axios'
import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon' import ExclamationOctagonIcon from '../../Icons/ExclamationOctagonIcon'
import ReloadIcon from '../../Icons/ReloadIcon' import ReloadIcon from '../../Icons/ReloadIcon'
import LockIcon from '../../Icons/LockIcon' import LockIcon from '../../Icons/LockIcon'
import config from '../../../config' import config from '../../../config'
import loglevel from 'loglevel' import loglevel from 'loglevel'
import { getModelByName } from '../../../database/ObjectModels' import { getModelByName } from '../../../database/ObjectModels'
import ProgressDisplay from '../common/ProgressDisplay' import ProgressDisplay from '../common/ProgressDisplay'
const logger = loglevel.getLogger('ApiServerContext') const logger = loglevel.getLogger('ApiServerContext')
@ -2921,27 +2920,19 @@ const ApiServerProvider = ({ children }) => {
{children} {children}
<Modal <Modal
title={ title={
!isReconnecting ? (
<Space size={'middle'}> <Space size={'middle'}>
<ExclamationOctagonIcon /> <ExclamationOctagonIcon />
Connection Lost Connection Lost
</Space> </Space>
) : (
false
)
} }
open={Boolean(token) && authenticated == true && connectionIssue} open={Boolean(token) && authenticated == true && connectionIssue}
style={{ maxWidth: !isReconnecting ? 480 : 260 }} style={{ maxWidth: 480 }}
zIndex={3000} zIndex={3000}
closable={false} closable={false}
height={isReconnecting ? 20 : undefined}
centered centered
className={isReconnecting ? 'loading-modal' : undefined}
maskClosable={false} maskClosable={false}
getContainer={() => document.body} getContainer={() => document.body}
footer={ footer={[
!isReconnecting
? [
<Button <Button
key='reconnect' key='reconnect'
loading={isReconnecting} loading={isReconnecting}
@ -2949,11 +2940,8 @@ const ApiServerProvider = ({ children }) => {
> >
Reconnect Reconnect
</Button> </Button>
] ]}
: false
}
> >
{!isReconnecting ? (
<Flex vertical gap='middle'> <Flex vertical gap='middle'>
<Text> <Text>
{isReconnecting {isReconnecting
@ -2968,12 +2956,6 @@ const ApiServerProvider = ({ children }) => {
status={'exception'} status={'exception'}
/> />
</Flex> </Flex>
) : (
<Space size={'middle'}>
<LoadingOutlined />
<Text style={{ margin: 0 }}>Reconnecting, please wait...</Text>
</Space>
)}
</Modal> </Modal>
<Modal <Modal
title={ title={

View File

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

View File

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