diff --git a/assets/stylesheets/App.css b/assets/stylesheets/App.css
index 15667e1b..0dae3b8 100644
--- a/assets/stylesheets/App.css
+++ b/assets/stylesheets/App.css
@@ -345,3 +345,8 @@ body {
.rollup-table .ant-table {
border-radius: 0px !important;
}
+
+.ant-select-selection-item .ant-tag,
+.ant-select-tree-title .ant-tag {
+ background: transparent !important;
+}
diff --git a/src/components/Dashboard/common/ObjectSelect.jsx b/src/components/Dashboard/common/ObjectSelect.jsx
index ecfc994..3666554 100644
--- a/src/components/Dashboard/common/ObjectSelect.jsx
+++ b/src/components/Dashboard/common/ObjectSelect.jsx
@@ -14,8 +14,16 @@ import { AuthContext } from '../context/AuthContext'
import ObjectProperty from './ObjectProperty'
import { getModelByName } from '../../../database/ObjectModels'
import merge from 'lodash/merge'
+import { getModelProperty } from '../../../database/ObjectModels'
const { SHOW_CHILD } = TreeSelect
+// Helper to check if two values are equal (handling objects/ids)
+const areValuesEqual = (v1, v2) => {
+ const id1 = v1 && typeof v1 === 'object' && v1._id ? v1._id : v1
+ const id2 = v2 && typeof v2 === 'object' && v2._id ? v2._id : v2
+ return String(id1) === String(id2)
+}
+
const ObjectSelect = ({
type = 'unknown',
showSearch = false,
@@ -31,7 +39,7 @@ const ObjectSelect = ({
const { token } = useContext(AuthContext)
// --- State ---
const [treeData, setTreeData] = useState([])
- const [objectPropertiesTree, setObjectPropertiesTree] = useState({})
+ const [objectPropertiesTree, setObjectPropertiesTree] = useState([])
const [initialized, setInitialized] = useState(false)
const [error, setError] = useState(false)
const properties = useMemo(() => getModelByName(type).group || [], [type])
@@ -69,6 +77,50 @@ const ObjectSelect = ({
[isMinimalObject, fetchObject, type]
)
+ const mergeGroups = useCallback((current, incoming) => {
+ if (!current) return incoming
+ if (!incoming) return current
+ if (!Array.isArray(current) || !Array.isArray(incoming)) return incoming
+
+ const merged = [...current]
+
+ // Helper to generate a unique key for a group node
+ const getGroupKey = (item) => {
+ const val = item.value
+ const valPart =
+ val && typeof val === 'object' && val._id
+ ? val._id
+ : JSON.stringify(val)
+ return `${item.property}:${valPart}`
+ }
+
+ for (const item of incoming) {
+ if (item.property && item.value !== undefined) {
+ // It's a group node
+ const itemKey = getGroupKey(item)
+ const existingIdx = merged.findIndex(
+ (x) =>
+ x.property && x.value !== undefined && getGroupKey(x) === itemKey
+ )
+
+ if (existingIdx > -1) {
+ merged[existingIdx] = {
+ ...merged[existingIdx],
+ children: mergeGroups(merged[existingIdx].children, item.children)
+ }
+ } else {
+ merged.push(item)
+ }
+ } else {
+ // It's a leaf object
+ if (!merged.some((x) => String(x._id) === String(item._id))) {
+ merged.push(item)
+ }
+ }
+ }
+ return merged
+ }, [])
+
// Fetch the object properties tree from the API
const handleFetchObjectsProperties = useCallback(
async (customFilter = filter) => {
@@ -78,11 +130,14 @@ const ObjectSelect = ({
filter: customFilter,
masterFilter
})
+
if (Array.isArray(data)) {
- setObjectPropertiesTree((prev) => merge([], prev, data))
+ setObjectPropertiesTree((prev) => mergeGroups(prev, data))
} else {
- setObjectPropertiesTree((prev) => merge({}, prev, data))
+ // Fallback if API returns something unexpected
+ setObjectPropertiesTree((prev) => merge([], prev, data))
}
+
setInitialLoading(false)
setError(false)
return data
@@ -92,24 +147,31 @@ const ObjectSelect = ({
return null
}
},
- [type, fetchObjectsByProperty, properties, filter, masterFilter]
+ [
+ type,
+ fetchObjectsByProperty,
+ properties,
+ filter,
+ masterFilter,
+ mergeGroups
+ ]
)
// Convert the API response to AntD TreeSelect treeData
const buildTreeData = useCallback(
(data, pIdx = 0, parentKeys = [], filterPath = []) => {
- if (!data) return []
- if (Array.isArray(data)) {
+ if (!data || !Array.isArray(data)) return []
+ console.log(data, pIdx, properties.length)
+ // If we are past the grouping properties, these are leaf objects
+ if (pIdx >= properties.length) {
return data.map((object) => {
setObjectList((prev) => {
- const filtered = prev.filter(
- (prevObject) => prevObject._id != object._id
- )
- return [...filtered, object]
+ if (prev.some((p) => p._id === object._id)) return prev
+ return [...prev, object]
})
return {
title: (
-
+
{
- if (property != null && typeof value === 'object') {
- const newFilterPath = filterPath.concat({ property, value: key })
- return {
- title: ,
- 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
- }
- }
+ // Group Nodes
+ return data
+ .map((group) => {
+ // Only process if it looks like a group
+ if (!group.property) return null
+
+ const { property, value, children } = group
+ var valueString = value
+ if (value && typeof value === 'object' && value._id) {
+ valueString = value._id
+ }
+ if (Array.isArray(valueString)) {
+ valueString = valueString.join(',')
+ }
+ const nodeKey = parentKeys
+ .concat(property + ':' + valueString)
+ .join('-')
+ const newFilterPath = filterPath.concat({
+ property,
+ value: valueString
})
- .filter(Boolean)
- }
+
+ const modelProperty = getModelProperty(type, property)
+ return {
+ title: ,
+ value: nodeKey,
+ key: nodeKey,
+ property,
+ filterValue: valueString,
+ parentKeys: parentKeys.concat(valueString),
+ filterPath: newFilterPath,
+ selectable: false,
+ isLeaf: false,
+ children: buildTreeData(
+ children,
+ pIdx + 1,
+ parentKeys.concat(valueString),
+ newFilterPath
+ )
+ }
+ })
+ .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
+ // node.property is the property name, node.value is the value key
if (!node.property) return
+ if (type == 'unknown') return
// Build filter for this node by merging all parent property-value pairs
const customFilter = { ...filter }
if (Array.isArray(node.filterPath)) {
@@ -172,26 +249,40 @@ const ObjectSelect = ({
customFilter[property] = value
})
}
+ // Ensure current node is in filter (should be covered by filterPath, but redundancy is safe)
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] || {}
+
+ // Navigate to the specific node's children in the response
+ let nodeSpecificChildren = data
+
+ if (node.filterPath && Array.isArray(node.filterPath)) {
+ for (const pathItem of node.filterPath) {
+ if (!Array.isArray(nodeSpecificChildren)) break
+ const match = nodeSpecificChildren.find(
+ (g) =>
+ g.property === pathItem.property &&
+ areValuesEqual(g.value, pathItem.value)
+ )
+ if (match) {
+ nodeSpecificChildren = match.children
+ } else {
+ nodeSpecificChildren = []
+ break
+ }
+ }
}
+
// Build new tree children only for this specific node
const children = buildTreeData(
- nodeSpecificData,
+ nodeSpecificChildren,
properties.indexOf(node.property) + 1,
node.parentKeys || [],
- (node.filterPath || []).concat({
- property: node.property,
- value: node.filterValue
- })
+ node.filterPath
)
+
// Update treeData with new children for this node only
setTreeData((prevTreeData) => {
// Helper to recursively update the correct node
@@ -250,7 +341,8 @@ const ObjectSelect = ({
value &&
typeof value === 'object' &&
value !== null &&
- !initialized
+ !initialized &&
+ type != 'unknown'
) {
// Check if value is a minimal object and fetch full object if needed
const fullValue = await fetchFullObjectIfNeeded(value)
@@ -260,7 +352,13 @@ const ObjectSelect = ({
properties.forEach((prop) => {
if (Object.prototype.hasOwnProperty.call(fullValue, prop)) {
const filterValue = fullValue[prop]
- if (filterValue?.name) {
+ if (
+ filterValue &&
+ typeof filterValue === 'object' &&
+ filterValue._id
+ ) {
+ valueFilter[prop] = filterValue._id
+ } else if (filterValue?.name) {
valueFilter[prop] = filterValue.name
} else if (Array.isArray(filterValue)) {
valueFilter[prop] = filterValue.join(',')
@@ -275,12 +373,13 @@ const ObjectSelect = ({
setInitialized(true)
return
}
- if (!initialized && token != null) {
+ if (!initialized && token != null && type != 'unknown') {
handleFetchObjectsProperties()
setInitialized(true)
}
- if (value == null) {
+ if (value == null || type == 'unknown') {
setTreeSelectValue(null)
+ setInitialLoading(false)
setInitialized(true)
}
}
@@ -292,7 +391,8 @@ const ObjectSelect = ({
handleFetchObjectsProperties,
initialized,
token,
- fetchFullObjectIfNeeded
+ fetchFullObjectIfNeeded,
+ type
])
const prevValuesRef = useRef({ type, masterFilter })
@@ -341,6 +441,14 @@ const ObjectSelect = ({
}
}, [value])
+ const placeholder = useMemo(
+ () =>
+ type == 'unknown'
+ ? 'n/a'
+ : `Select a ${getModelByName(type).label.toLowerCase()}...`,
+ [type]
+ )
+
// --- Error UI ---
if (error) {
return (
@@ -373,12 +481,12 @@ const ObjectSelect = ({
multiple={multiple}
loadData={loadData}
showCheckedStrategy={SHOW_CHILD}
- placeholder={`Select a ${getModelByName(type).label.toLowerCase()}...`}
+ placeholder={placeholder}
{...treeSelectProps}
{...rest}
value={treeSelectValue}
onChange={onTreeSelectChange}
- disabled={disabled}
+ disabled={disabled || type == 'unknown'}
/>
)
}
diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx
index 5a1dcdf..cf39a72 100644
--- a/src/components/Dashboard/context/ApiServerContext.jsx
+++ b/src/components/Dashboard/context/ApiServerContext.jsx
@@ -665,7 +665,12 @@ const ApiServerProvider = ({ children }) => {
`${config.backendUrl}/${type.toLowerCase()}s/properties`,
{
params: {
- ...filter,
+ ...Object.keys(filter).reduce((acc, key) => {
+ acc[key] = Array.isArray(filter[key])
+ ? filter[key].join(',')
+ : filter[key]
+ return acc
+ }, {}),
properties: properties.join(','), // Convert array to comma-separated string
masterFilter: JSON.stringify(masterFilter)
},