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) },