Enhance Filter Functionality Across Dashboard Components
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
- Updated FilterInput, FilterSidebar, ObjectTable, SimpleDateTimePropertyFilter, and SimplePropertyFilter components to support new filter and masterFilter props, improving data filtering capabilities. - Introduced ObjectTableFilterContext for better state management of filter values across components, enhancing the overall user experience. - Refactored getModelPropertyValues function in ApiServerContext to accept filter parameters, allowing for more dynamic data fetching based on user-defined filters. - Improved loading and visibility handling in SimpleDateTimePropertyFilter and SimplePropertyFilter to optimize performance during data retrieval.
This commit is contained in:
parent
f447ec287a
commit
0ae4f1fae0
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -1146,7 +1152,8 @@ const FilterInput = ({
|
||||
{propertyFilterEnabled && (
|
||||
<Popover
|
||||
open={focused}
|
||||
content={propertyFilterContent}
|
||||
destroyOnHidden={true}
|
||||
content={focused ? propertyFilterContent : null}
|
||||
placement='bottomLeft'
|
||||
arrow={false}
|
||||
trigger={[]}
|
||||
@ -1191,7 +1198,9 @@ FilterInput.propTypes = {
|
||||
propertyFilter: PropTypes.shape({
|
||||
modelType: PropTypes.string.isRequired,
|
||||
propertyName: PropTypes.string.isRequired
|
||||
})
|
||||
}),
|
||||
filter: PropTypes.object,
|
||||
masterFilter: PropTypes.object
|
||||
}
|
||||
|
||||
export default FilterInput
|
||||
|
||||
@ -215,6 +215,8 @@ const FilterSidebar = ({
|
||||
modelType: type,
|
||||
propertyName: row.field
|
||||
}}
|
||||
filter={localFilter}
|
||||
masterFilter={masterFilter}
|
||||
/>
|
||||
<Button
|
||||
icon={<CloseOutlined />}
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</div>
|
||||
</ScrollBox>
|
||||
@ -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(
|
||||
</Flex>
|
||||
)
|
||||
|
||||
return tableContent
|
||||
return (
|
||||
<ObjectTableFilterContext.Provider value={tableFilterContextValue}>
|
||||
{tableContent}
|
||||
</ObjectTableFilterContext.Provider>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react'
|
||||
|
||||
export const ObjectTableFilterContext = createContext(null)
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 []
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user