diff --git a/src/components/Dashboard/common/FilterInput.jsx b/src/components/Dashboard/common/FilterInput.jsx
index 5d323114..041d7693 100644
--- a/src/components/Dashboard/common/FilterInput.jsx
+++ b/src/components/Dashboard/common/FilterInput.jsx
@@ -442,7 +442,9 @@ const FilterInput = ({
onBlur,
onPressEnter,
size,
- propertyFilter = null
+ propertyFilter = null,
+ filter = {},
+ masterFilter = {}
}) => {
const { getPrefixCls, direction } = useContext(ConfigProvider.ConfigContext)
const prefixCls = getPrefixCls('input')
@@ -895,6 +897,10 @@ const FilterInput = ({
propertyName={propertyFilter.propertyName}
value={propertyFilterValue}
onChange={handlePropertyFilterChange}
+ filter={filter}
+ masterFilter={masterFilter}
+ visible={true}
+ useTableFilter={false}
/>
@@ -1146,7 +1152,8 @@ const FilterInput = ({
{propertyFilterEnabled && (
}
diff --git a/src/components/Dashboard/common/ObjectTable.jsx b/src/components/Dashboard/common/ObjectTable.jsx
index 7d070834..6c4e39c1 100644
--- a/src/components/Dashboard/common/ObjectTable.jsx
+++ b/src/components/Dashboard/common/ObjectTable.jsx
@@ -56,6 +56,7 @@ import {
} from '../context/TableStateContext'
import { hasActionPermission } from '../../../database/permissions'
import Tooltip from './Tooltip'
+import { ObjectTableFilterContext } from './ObjectTableFilterContext'
const logger = loglevel.getLogger('DasboardTable')
logger.setLevel(config.logLevel)
@@ -98,7 +99,9 @@ const ColumnFilterDropdown = ({
visible,
propertyName,
propertyLabel,
- modelType
+ modelType,
+ filter = {},
+ masterFilter = {}
}) => {
const [expression, setExpression] = useState('')
const [draft, setDraft] = useState(selectedKeys || [])
@@ -164,6 +167,9 @@ const ColumnFilterDropdown = ({
propertyName={propertyName}
value={draft}
onChange={handleDraftChange}
+ filter={filter}
+ masterFilter={masterFilter}
+ visible={visible}
/>
@@ -181,7 +187,9 @@ ColumnFilterDropdown.propTypes = {
visible: PropTypes.bool,
propertyName: PropTypes.string,
propertyLabel: PropTypes.string,
- modelType: PropTypes.string
+ modelType: PropTypes.string,
+ filter: PropTypes.object,
+ masterFilter: PropTypes.object
}
const RowForm = ({ record, isEditing, onRegister, children }) => {
@@ -867,6 +875,14 @@ const ObjectTable = forwardRef(
return { ...active, ...masterFilter }
}, [sidebarFilter, masterFilter])
+ const tableFilterContextValue = useMemo(
+ () => ({
+ filter: getActiveFilterValues(sidebarFilter),
+ masterFilter
+ }),
+ [sidebarFilter, masterFilter]
+ )
+
newEventHandlerRef.current = newEventHandler
subscriptionFilterRef.current = subscriptionFilter
subscribeToObjectTypeUpdatesFnRef.current = subscribeToObjectTypeUpdates
@@ -1112,6 +1128,8 @@ const ObjectTable = forwardRef(
propertyName={propertyName}
propertyLabel={propertyLabel}
modelType={type}
+ filter={sidebarFilter}
+ masterFilter={masterFilter}
/>
)
@@ -1583,7 +1601,11 @@ const ObjectTable = forwardRef(
)
- return tableContent
+ return (
+
+ {tableContent}
+
+ )
}
)
diff --git a/src/components/Dashboard/common/ObjectTableFilterContext.jsx b/src/components/Dashboard/common/ObjectTableFilterContext.jsx
new file mode 100644
index 00000000..d3d47dae
--- /dev/null
+++ b/src/components/Dashboard/common/ObjectTableFilterContext.jsx
@@ -0,0 +1,3 @@
+import { createContext } from 'react'
+
+export const ObjectTableFilterContext = createContext(null)
diff --git a/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx b/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx
index e1b90967..25f3bb25 100644
--- a/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx
+++ b/src/components/Dashboard/common/SimpleDateTimePropertyFilter.jsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, useContext, useMemo, useCallback } from 'react'
+import { useState, useEffect, useContext, useMemo, useCallback, useRef } from 'react'
import { Spin, Tree, Flex, Checkbox, Typography } from 'antd'
import PropTypes from 'prop-types'
import dayjs from 'dayjs'
@@ -369,31 +369,94 @@ const expandKeysForChecked = (keys, nodeByKey) => {
return [...checked]
}
+const EMPTY_OBJECT = {}
+
+const stableStringify = (value) => {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(',')}]`
+ }
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(',')}}`
+ }
+ return JSON.stringify(value)
+}
+
+const getFilterHash = (filter, masterFilter) =>
+ `${stableStringify(filter || {})}:${stableStringify(masterFilter || {})}`
+
const SimpleDateTimePropertyFilter = ({
modelType,
propertyName,
value = [],
onChange,
- search = ''
+ search = '',
+ filter = EMPTY_OBJECT,
+ masterFilter = EMPTY_OBJECT,
+ visible = true
}) => {
const { getModelPropertyValues } = useContext(ApiServerContext)
+ const getModelPropertyValuesRef = useRef(getModelPropertyValues)
+ getModelPropertyValuesRef.current = getModelPropertyValues
+
const [dates, setDates] = useState([])
const [loading, setLoading] = useState(false)
const [localChecked, setLocalChecked] = useState(null)
+ const filterForValues = useMemo(() => {
+ const next = { ...(filter || {}) }
+ if (propertyName) delete next[propertyName]
+ return next
+ }, [filter, propertyName])
+
+ const masterFilterForValues = masterFilter || EMPTY_OBJECT
+
+ const filterHash = useMemo(
+ () => getFilterHash(filterForValues, masterFilterForValues),
+ [filterForValues, masterFilterForValues]
+ )
+ const loadedFilterHashRef = useRef(null)
+ const filterHashRef = useRef(filterHash)
+ const filterForValuesRef = useRef(filterForValues)
+ const masterFilterForValuesRef = useRef(masterFilterForValues)
+ const wasVisibleRef = useRef(false)
+ filterHashRef.current = filterHash
+ filterForValuesRef.current = filterForValues
+ masterFilterForValuesRef.current = masterFilterForValues
+
useEffect(() => {
+ const becameVisible = visible && !wasVisibleRef.current
+ wasVisibleRef.current = visible
+ if (!visible) return
+ if (!becameVisible && loadedFilterHashRef.current != null) return
+
let cancelled = false
const loadOptions = async () => {
if (!modelType || !propertyName) return
+
+ const currentHash = filterHashRef.current
+ if (loadedFilterHashRef.current === currentHash) return
+
+ setDates([])
setLoading(true)
try {
- const values = await getModelPropertyValues(modelType, propertyName)
+ const values = await getModelPropertyValuesRef.current(
+ modelType,
+ propertyName,
+ {
+ filter: filterForValuesRef.current,
+ masterFilter: masterFilterForValuesRef.current
+ }
+ )
if (cancelled) return
const parsed = (values || [])
.map(parseDateValue)
.filter(Boolean)
.sort((a, b) => a.getTime() - b.getTime())
+ loadedFilterHashRef.current = currentHash
setDates(parsed)
} finally {
if (!cancelled) setLoading(false)
@@ -404,7 +467,7 @@ const SimpleDateTimePropertyFilter = ({
return () => {
cancelled = true
}
- }, [modelType, propertyName, getModelPropertyValues])
+ }, [visible, modelType, propertyName])
const treeData = useMemo(
() => buildTreeData(buildDateHierarchy(dates)),
@@ -569,7 +632,10 @@ SimpleDateTimePropertyFilter.propTypes = {
propertyName: PropTypes.string,
value: PropTypes.array,
onChange: PropTypes.func,
- search: PropTypes.string
+ search: PropTypes.string,
+ filter: PropTypes.object,
+ masterFilter: PropTypes.object,
+ visible: PropTypes.bool
}
export default SimpleDateTimePropertyFilter
diff --git a/src/components/Dashboard/common/SimplePropertyFilter.jsx b/src/components/Dashboard/common/SimplePropertyFilter.jsx
index bf6f7f92..0fa1f646 100644
--- a/src/components/Dashboard/common/SimplePropertyFilter.jsx
+++ b/src/components/Dashboard/common/SimplePropertyFilter.jsx
@@ -2,6 +2,7 @@ import { useState, useEffect, useContext, useMemo, useRef } from 'react'
import { Flex, Checkbox, Spin, Typography } from 'antd'
import PropTypes from 'prop-types'
import { ApiServerContext } from '../context/ApiServerContext'
+import { ObjectTableFilterContext } from './ObjectTableFilterContext'
import {
getModelByName,
getModelProperties
@@ -50,27 +51,91 @@ const matchOptions = (options, selected) => {
return options.filter((option) => selectedKeys.has(getOptionKey(option)))
}
+const stableStringify = (value) => {
+ if (Array.isArray(value)) {
+ return `[${value.map(stableStringify).join(',')}]`
+ }
+ if (value && typeof value === 'object') {
+ return `{${Object.keys(value)
+ .sort()
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
+ .join(',')}}`
+ }
+ return JSON.stringify(value)
+}
+
+const getFilterHash = (filter, masterFilter) =>
+ `${stableStringify(filter || {})}:${stableStringify(masterFilter || {})}`
+
+const omitUndefinedValues = (obj) =>
+ Object.fromEntries(
+ Object.entries(obj || {}).filter(
+ ([, value]) => value !== undefined && value !== ''
+ )
+ )
+
// Survive table reloads / popover remounts without refetching the same property.
const optionsCache = new Map()
+const lastLoadedFilterHashByProperty = new Map()
+
+const EMPTY_OBJECT = {}
const SimplePropertyFilter = ({
modelType,
propertyName,
value = [],
onChange,
- search = ''
+ search = '',
+ filter = EMPTY_OBJECT,
+ masterFilter = EMPTY_OBJECT,
+ visible = true,
+ useTableFilter = true
}) => {
const { getModelPropertyValues } = useContext(ApiServerContext)
+ const tableFilterContext = useContext(ObjectTableFilterContext)
const getModelPropertyValuesRef = useRef(getModelPropertyValues)
getModelPropertyValuesRef.current = getModelPropertyValues
+ const resolvedFilter = useTableFilter
+ ? (tableFilterContext?.filter ?? filter)
+ : filter
+ const resolvedMasterFilter = useTableFilter
+ ? (tableFilterContext?.masterFilter ?? masterFilter)
+ : masterFilter || tableFilterContext?.masterFilter || EMPTY_OBJECT
+
+ const filterForValues = useMemo(() => {
+ const next = omitUndefinedValues(resolvedFilter)
+ if (propertyName) delete next[propertyName]
+ return next
+ }, [resolvedFilter, propertyName])
+
+ const masterFilterForValues = useMemo(
+ () => omitUndefinedValues(resolvedMasterFilter),
+ [resolvedMasterFilter]
+ )
+
+ const filterHash = useMemo(
+ () => getFilterHash(filterForValues, masterFilterForValues),
+ [filterForValues, masterFilterForValues]
+ )
+ const loadedFilterHashRef = useRef(null)
+ const filterHashRef = useRef(filterHash)
+ const filterForValuesRef = useRef(filterForValues)
+ const masterFilterForValuesRef = useRef(masterFilterForValues)
+ const wasVisibleRef = useRef(false)
+ filterHashRef.current = filterHash
+ filterForValuesRef.current = filterForValues
+ masterFilterForValuesRef.current = masterFilterForValues
+
const cacheKey =
- modelType && propertyName ? `${modelType}:${propertyName}` : null
+ modelType && propertyName
+ ? `${modelType}:${propertyName}:${filterHash}`
+ : null
const [options, setOptions] = useState(
() => (cacheKey && optionsCache.get(cacheKey)) || []
)
const [loading, setLoading] = useState(
- () => !(cacheKey && optionsCache.has(cacheKey))
+ () => visible && !(cacheKey && optionsCache.has(cacheKey))
)
const [localChecked, setLocalChecked] = useState(null)
@@ -81,6 +146,11 @@ const SimplePropertyFilter = ({
)
useEffect(() => {
+ const becameVisible = visible && !wasVisibleRef.current
+ wasVisibleRef.current = visible
+ if (!visible) return
+ if (!becameVisible && loadedFilterHashRef.current != null) return
+
let cancelled = false
const loadOptions = async () => {
@@ -89,25 +159,40 @@ const SimplePropertyFilter = ({
return
}
- const key = `${modelType}:${propertyName}`
- const cached = optionsCache.get(key)
- if (cached) {
- setOptions(cached)
- setLoading(false)
- return
+ const currentHash = filterHashRef.current
+ const propertyKey = `${modelType}:${propertyName}`
+ const lastHash =
+ loadedFilterHashRef.current ??
+ lastLoadedFilterHashByProperty.get(propertyKey)
+
+ if (lastHash === currentHash) {
+ loadedFilterHashRef.current = currentHash
+ const cached = optionsCache.get(`${propertyKey}:${currentHash}`)
+ if (cached) {
+ setOptions(cached)
+ setLoading(false)
+ return
+ }
}
+ setOptions([])
setLoading(true)
try {
const values = await getModelPropertyValuesRef.current(
modelType,
- propertyName
+ propertyName,
+ {
+ filter: filterForValuesRef.current,
+ masterFilter: masterFilterForValuesRef.current
+ }
)
if (cancelled) return
const unique = [
...new Set((values || []).filter((v) => v != null && v !== ''))
]
- optionsCache.set(key, unique)
+ optionsCache.set(`${propertyKey}:${currentHash}`, unique)
+ lastLoadedFilterHashByProperty.set(propertyKey, currentHash)
+ loadedFilterHashRef.current = currentHash
setOptions(unique)
} finally {
if (!cancelled) setLoading(false)
@@ -118,9 +203,8 @@ const SimplePropertyFilter = ({
return () => {
cancelled = true
}
- // Intentionally omit getModelPropertyValues — context recreates it on every
- // provider render (e.g. table fetchLoading), which would refetch endlessly.
- }, [modelType, propertyName, property])
+ // Reload only when this filter becomes visible, then compare filterHash.
+ }, [visible, modelType, propertyName, property])
const valueKey = useMemo(
() => JSON.stringify((value || []).map(getOptionKey)),
@@ -179,6 +263,9 @@ const SimplePropertyFilter = ({
value={value}
onChange={onChange}
search={search}
+ filter={resolvedFilter}
+ masterFilter={resolvedMasterFilter}
+ visible={visible}
/>
)
}
@@ -225,7 +312,11 @@ SimplePropertyFilter.propTypes = {
propertyName: PropTypes.string,
value: PropTypes.array,
onChange: PropTypes.func,
- search: PropTypes.string
+ search: PropTypes.string,
+ filter: PropTypes.object,
+ masterFilter: PropTypes.object,
+ visible: PropTypes.bool,
+ useTableFilter: PropTypes.bool
}
export default SimplePropertyFilter
diff --git a/src/components/Dashboard/context/ApiServerContext.jsx b/src/components/Dashboard/context/ApiServerContext.jsx
index 369d5782..a201d052 100644
--- a/src/components/Dashboard/context/ApiServerContext.jsx
+++ b/src/components/Dashboard/context/ApiServerContext.jsx
@@ -1874,17 +1874,29 @@ const ApiServerProvider = ({ children }) => {
}
}
- const getModelPropertyValues = async (objectType, property) => {
+ const getModelPropertyValues = async (objectType, property, params = {}) => {
+ const { filter = {}, masterFilter = {} } = params
+
logger.debug(
'Fetching property values for model type:',
objectType,
- property
+ property,
+ { filter, masterFilter }
)
try {
const response = await axios.get(
`${config.backendUrl}/${getObjectEndpoint(objectType)}/values`,
{
- params: { property },
+ params: {
+ property,
+ ...Object.keys(filter).reduce((acc, key) => {
+ acc[key] = Array.isArray(filter[key])
+ ? filter[key].join(',')
+ : filter[key]
+ return acc
+ }, {}),
+ masterFilter: JSON.stringify(masterFilter)
+ },
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`
@@ -1900,7 +1912,7 @@ const ApiServerProvider = ({ children }) => {
} catch (err) {
console.error(err)
showError(err, () => {
- getModelPropertyValues(objectType, property)
+ getModelPropertyValues(objectType, property, params)
})
return []
}