2025-08-22 22:18:50 +01:00

296 lines
9.2 KiB
JavaScript

import { useEffect, useState, useContext, useCallback, useMemo } from 'react'
import PropTypes from 'prop-types'
import { TreeSelect, Space, Button, Input } from 'antd'
import ReloadIcon from '../../Icons/ReloadIcon'
import { ApiServerContext } from '../context/ApiServerContext'
import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty'
import { getModelByName } from '../../../database/ObjectModels'
import merge from 'lodash/merge'
const { SHOW_CHILD } = TreeSelect
const ObjectSelect = ({
type = 'unknown',
showSearch = false,
multiple = false,
treeSelectProps = {},
filter = {},
masterFilter = {},
value,
disabled = false,
...rest
}) => {
const { fetchObjectsByProperty } = useContext(ApiServerContext)
const { token } = useContext(AuthContext)
// --- State ---
const [treeData, setTreeData] = useState([])
const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
const [initialized, setInitialized] = useState(false)
const [error, setError] = useState(false)
const properties = useMemo(() => getModelByName(type).group || [], [type])
const [objectList, setObjectList] = useState([])
const [treeSelectValue, setTreeSelectValue] = useState(null)
const [initialLoading, setInitialLoading] = useState(true)
// Fetch the object properties tree from the API
const handleFetchObjectsProperties = useCallback(
async (customFilter = filter) => {
try {
const data = await fetchObjectsByProperty(type, {
properties: properties,
filter: customFilter,
masterFilter
})
if (Array.isArray(data)) {
setObjectPropertiesTree((prev) => merge([], prev, data))
} else {
setObjectPropertiesTree((prev) => merge({}, prev, data))
}
setInitialLoading(false)
setError(false)
return data
} catch (err) {
console.error(err)
setError(true)
return null
}
},
[type, fetchObjectsByProperty, properties, filter, masterFilter]
)
// Convert the API response to AntD TreeSelect treeData
const buildTreeData = useCallback(
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
if (!data) return []
if (Array.isArray(data)) {
return data.map((object) => {
setObjectList((prev) => {
const filtered = prev.filter(
(prevObject) => prevObject._id != object._id
)
return [...filtered, object]
})
return {
title: (
<ObjectProperty
key={object._id}
type='object'
value={object}
objectType={type}
objectData={object}
isEditing={false}
/>
),
value: object._id,
key: object._id,
isLeaf: true,
property: properties[pIdx - 1], // previous property
parentKeys,
filterPath
}
})
}
if (typeof data == 'object') {
const property = properties[pIdx] || null
return Object.entries(data)
.map(([key, value]) => {
if (property != null && typeof value === 'object') {
const newFilterPath = filterPath.concat({ property, value: key })
return {
title: <ObjectProperty type={property} value={key} />,
value: parentKeys.concat(key).join('-'),
filterValue: key,
key: parentKeys.concat(key).join('-'),
property,
parentKeys: parentKeys.concat(key || '-'),
filterPath: newFilterPath,
selectable: false,
children: buildTreeData(
value,
pIdx + 1,
parentKeys.concat(key),
newFilterPath
),
isLeaf: false
}
}
})
.filter(Boolean)
}
},
[properties, type]
)
// --- loadData for async loading on expand ---
const loadData = async (node) => {
// node.property is the property name, node.value is the value
if (!node.property) return
// Build filter for this node by merging all parent property-value pairs
const customFilter = { ...filter }
if (Array.isArray(node.filterPath)) {
node.filterPath.forEach(({ property, value }) => {
customFilter[property] = value
})
}
customFilter[node.property] = node.filterValue
// Fetch children for this node
const data = await handleFetchObjectsProperties(customFilter)
if (!data) return
// Extract only the children for the specific node that was expanded
let nodeSpecificData = data
if (typeof data === 'object' && !Array.isArray(data)) {
// If the API returns an object with multiple keys, get only the data for this node
nodeSpecificData = data[node.value] || {}
}
// Build new tree children only for this specific node
const children = buildTreeData(
nodeSpecificData,
properties.indexOf(node.property) + 1,
node.parentKeys || [],
(node.filterPath || []).concat({
property: node.property,
value: node.filterValue
})
)
// Update treeData with new children for this node only
setTreeData((prevTreeData) => {
// Helper to recursively update the correct node
const updateNode = (nodes) =>
nodes.map((n) => {
if (n.key === node.key) {
return { ...n, children, isLeaf: children.length === 0 }
} else if (n.children) {
return { ...n, children: updateNode(n.children) }
}
return n
})
return updateNode(prevTreeData)
})
}
const onTreeSelectChange = (value) => {
// value can be a string (single) or array (multiple)
if (multiple) {
// Multiple selection
let selectedObjects = []
if (Array.isArray(value)) {
selectedObjects = value
.map((id) => objectList.find((obj) => obj._id === id))
.filter(Boolean)
}
setTreeSelectValue(value)
if (rest.onChange) rest.onChange(selectedObjects)
} else {
// Single selection
const selectedObject = objectList.find((obj) => obj._id === value)
setTreeSelectValue(value)
if (rest.onChange) rest.onChange(selectedObject)
}
}
// Update treeData when objectPropertiesTree changes
useEffect(() => {
if (objectPropertiesTree && Object.keys(objectPropertiesTree).length > 0) {
const newTreeData = buildTreeData(objectPropertiesTree)
setTreeData((prev) => {
if (JSON.stringify(prev) !== JSON.stringify(newTreeData)) {
return newTreeData
}
return prev
})
}
}, [objectPropertiesTree, properties, type, buildTreeData])
useEffect(() => {
if (value && typeof value === 'object' && value !== null && !initialized) {
// Build a new filter from value's properties that are in the properties list
const valueFilter = { ...filter }
properties.forEach((prop) => {
if (Object.prototype.hasOwnProperty.call(value, prop)) {
const filterValue = value[prop]
if (filterValue?.name) {
valueFilter[prop] = filterValue.name
} else if (Array.isArray(filterValue)) {
valueFilter[prop] = filterValue.join(',')
} else {
valueFilter[prop] = filterValue
}
}
})
// Fetch with the new filter
handleFetchObjectsProperties(valueFilter)
setTreeSelectValue(value._id)
setInitialized(true)
return
}
if (!initialized && token != null) {
handleFetchObjectsProperties()
setInitialized(true)
}
}, [
value,
filter,
properties,
handleFetchObjectsProperties,
initialized,
token
])
// --- Error UI ---
if (error) {
return (
<Space.Compact style={{ width: '100%' }}>
<Input value='Failed to load data.' status='error' disabled />
<Button
icon={<ReloadIcon />}
onClick={() => {
setError(false)
setTreeData([])
setInitialized(false)
}}
danger
/>
</Space.Compact>
)
}
if (initialLoading) {
return <TreeSelect disabled loading placeholder='Loading...' />
}
// --- Main TreeSelect UI ---
return (
<TreeSelect
treeDataSimpleMode={false}
treeDefaultExpandAll={true}
treeData={treeData}
showSearch={showSearch}
multiple={multiple}
loadData={loadData}
showCheckedStrategy={SHOW_CHILD}
placeholder={`Select a ${getModelByName(type).label.toLowerCase()}...`}
{...treeSelectProps}
{...rest}
value={treeSelectValue}
onChange={onTreeSelectChange}
disabled={disabled}
/>
)
}
ObjectSelect.propTypes = {
properties: PropTypes.arrayOf(PropTypes.string).isRequired,
filter: PropTypes.object,
masterFilter: PropTypes.object,
useFilter: PropTypes.bool,
value: PropTypes.any,
onChange: PropTypes.func,
showSearch: PropTypes.bool,
multiple: PropTypes.bool,
treeSelectProps: PropTypes.object,
type: PropTypes.string.isRequired,
disabled: PropTypes.bool
}
export default ObjectSelect