Enhance FilterInput and FilterSidebar with property filter support
- Updated FilterInput to include property filter functionality, allowing for more complex filtering expressions. - Introduced new utility functions to convert between filter expressions and values, improving data handling. - Modified FilterSidebar to pass property filter configurations to the FilterInput, enhancing the filtering capabilities. - Improved SimplePropertyFilter to cache options and prevent unnecessary refetching, optimizing performance during interactions.
This commit is contained in:
parent
6b848d392c
commit
18ac93d8e9
@ -1,5 +1,5 @@
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { ConfigProvider, Tag, theme } from 'antd'
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ConfigProvider, Popover, Tag, theme } from 'antd'
|
||||
import { CloseCircleFilled } from '@ant-design/icons'
|
||||
import PropTypes from 'prop-types'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
@ -9,6 +9,8 @@ import useSize from 'antd/es/config-provider/hooks/useSize'
|
||||
import { useCompactItemContext } from 'antd/es/space/Compact'
|
||||
import useStyle, { useSharedStyle } from 'antd/es/input/style'
|
||||
import { useThemeContext } from '../context/ThemeContext'
|
||||
import SimplePropertyFilter from './SimplePropertyFilter'
|
||||
import ScrollBox from './ScrollBox'
|
||||
|
||||
// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">".
|
||||
// Wildcards (* ?) and @ stay as plain text — they are not operands.
|
||||
@ -327,6 +329,25 @@ const chipsHaveLiveRoots = (root, roots) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const toFilterExpression = (values) => {
|
||||
if (!values?.length) return ''
|
||||
const parts = values.map((value) => {
|
||||
if (value && typeof value === 'object') {
|
||||
return String(value._id ?? value.type ?? JSON.stringify(value))
|
||||
}
|
||||
return String(value)
|
||||
})
|
||||
return parts.length === 1 ? parts[0] : parts.join('|')
|
||||
}
|
||||
|
||||
const fromFilterExpression = (expr) => {
|
||||
if (expr === undefined || expr === null || expr === '') return []
|
||||
if (typeof expr === 'string' && expr.includes('|')) {
|
||||
return expr.split('|').filter((part) => part !== '')
|
||||
}
|
||||
return [expr]
|
||||
}
|
||||
|
||||
const FilterInput = ({
|
||||
value = '',
|
||||
onChange,
|
||||
@ -338,7 +359,8 @@ const FilterInput = ({
|
||||
onFocus,
|
||||
onBlur,
|
||||
onPressEnter,
|
||||
size
|
||||
size,
|
||||
propertyFilter = null
|
||||
}) => {
|
||||
const { getPrefixCls, direction } = useContext(ConfigProvider.ConfigContext)
|
||||
const prefixCls = getPrefixCls('input')
|
||||
@ -734,6 +756,46 @@ const FilterInput = ({
|
||||
editorRef.current?.focus()
|
||||
}
|
||||
|
||||
const propertyFilterEnabled =
|
||||
!disabled &&
|
||||
propertyFilter?.modelType != null &&
|
||||
propertyFilter?.propertyName != null
|
||||
|
||||
const propertyFilterValue = useMemo(
|
||||
() => fromFilterExpression(internalValue),
|
||||
[internalValue]
|
||||
)
|
||||
|
||||
const handlePropertyFilterChange = useCallback(
|
||||
(keys) => {
|
||||
const next = toFilterExpression(keys)
|
||||
paint(next, focusedRef.current ? next.length : null)
|
||||
emitChange(next)
|
||||
},
|
||||
[emitChange, paint]
|
||||
)
|
||||
|
||||
const propertyFilterContent = propertyFilterEnabled ? (
|
||||
<div
|
||||
onMouseDown={(event) => {
|
||||
// Keep the input focused while interacting with the popover.
|
||||
event.preventDefault()
|
||||
}}
|
||||
style={{ width: 280, height: 220, margin: -4 }}
|
||||
>
|
||||
<ScrollBox inner smallPadding>
|
||||
<div style={{ padding: '12px 16px', minWidth: 0 }}>
|
||||
<SimplePropertyFilter
|
||||
modelType={propertyFilter.modelType}
|
||||
propertyName={propertyFilter.propertyName}
|
||||
value={propertyFilterValue}
|
||||
onChange={handlePropertyFilterChange}
|
||||
/>
|
||||
</div>
|
||||
</ScrollBox>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
// Auto-scroll while click-dragging a selection past the visible edges.
|
||||
useEffect(() => {
|
||||
let raf = 0
|
||||
@ -828,8 +890,7 @@ const FilterInput = ({
|
||||
? (token.paddingInlineSM ?? token.paddingXS)
|
||||
: (token.paddingInline ?? token.paddingSM)
|
||||
|
||||
return wrapSharedCSSVar(
|
||||
wrapCSSVar(
|
||||
const input = (
|
||||
<span
|
||||
className={classNames(
|
||||
affixCls,
|
||||
@ -969,6 +1030,28 @@ const FilterInput = ({
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
|
||||
return wrapSharedCSSVar(
|
||||
wrapCSSVar(
|
||||
propertyFilterEnabled ? (
|
||||
<Popover
|
||||
open={focused}
|
||||
content={propertyFilterContent}
|
||||
placement='bottomLeft'
|
||||
arrow={false}
|
||||
trigger={[]}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 8
|
||||
}
|
||||
}}
|
||||
>
|
||||
{input}
|
||||
</Popover>
|
||||
) : (
|
||||
input
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -983,7 +1066,11 @@ FilterInput.propTypes = {
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onPressEnter: PropTypes.func,
|
||||
size: PropTypes.oneOf(['small', 'middle', 'large'])
|
||||
size: PropTypes.oneOf(['small', 'middle', 'large']),
|
||||
propertyFilter: PropTypes.shape({
|
||||
modelType: PropTypes.string.isRequired,
|
||||
propertyName: PropTypes.string.isRequired
|
||||
})
|
||||
}
|
||||
|
||||
export default FilterInput
|
||||
|
||||
@ -39,7 +39,17 @@ const FilterSidebar = ({
|
||||
if (initialEmptyFields.current.has(k) && v === '') continue
|
||||
visible[k] = v
|
||||
}
|
||||
setLocalFilter(visible)
|
||||
setLocalFilter((prev) => {
|
||||
const prevKeys = Object.keys(prev)
|
||||
const nextKeys = Object.keys(visible)
|
||||
if (
|
||||
prevKeys.length === nextKeys.length &&
|
||||
nextKeys.every((key) => prev[key] === visible[key])
|
||||
) {
|
||||
return prev
|
||||
}
|
||||
return visible
|
||||
})
|
||||
}, [filter])
|
||||
|
||||
const debouncedFilterChange = useCallback(
|
||||
@ -167,6 +177,10 @@ const FilterSidebar = ({
|
||||
value={row.value}
|
||||
onChange={(value) => changeValue(row.field, value)}
|
||||
style={{ flex: 1 }}
|
||||
propertyFilter={{
|
||||
modelType: type,
|
||||
propertyName: row.field
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
icon={<CloseOutlined />}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useContext, useMemo } from 'react'
|
||||
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'
|
||||
@ -45,6 +45,9 @@ const matchOptions = (options, selected) => {
|
||||
return options.filter((option) => selectedKeys.has(getOptionKey(option)))
|
||||
}
|
||||
|
||||
// Survive table reloads / popover remounts without refetching the same property.
|
||||
const optionsCache = new Map()
|
||||
|
||||
const SimplePropertyFilter = ({
|
||||
modelType,
|
||||
propertyName,
|
||||
@ -53,8 +56,17 @@ const SimplePropertyFilter = ({
|
||||
search = ''
|
||||
}) => {
|
||||
const { getModelPropertyValues } = useContext(ApiServerContext)
|
||||
const [options, setOptions] = useState([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const getModelPropertyValuesRef = useRef(getModelPropertyValues)
|
||||
getModelPropertyValuesRef.current = getModelPropertyValues
|
||||
|
||||
const cacheKey =
|
||||
modelType && propertyName ? `${modelType}:${propertyName}` : null
|
||||
const [options, setOptions] = useState(
|
||||
() => (cacheKey && optionsCache.get(cacheKey)) || []
|
||||
)
|
||||
const [loading, setLoading] = useState(
|
||||
() => !(cacheKey && optionsCache.has(cacheKey))
|
||||
)
|
||||
const [localChecked, setLocalChecked] = useState(null)
|
||||
|
||||
const property = useMemo(
|
||||
@ -67,14 +79,30 @@ const SimplePropertyFilter = ({
|
||||
let cancelled = false
|
||||
|
||||
const loadOptions = async () => {
|
||||
if (!modelType || !propertyName || isDateTimeProperty(property)) return
|
||||
if (!modelType || !propertyName || isDateTimeProperty(property)) {
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const key = `${modelType}:${propertyName}`
|
||||
const cached = optionsCache.get(key)
|
||||
if (cached) {
|
||||
setOptions(cached)
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const values = await getModelPropertyValues(modelType, propertyName)
|
||||
const values = await getModelPropertyValuesRef.current(
|
||||
modelType,
|
||||
propertyName
|
||||
)
|
||||
if (cancelled) return
|
||||
const unique = [
|
||||
...new Set((values || []).filter((v) => v != null && v !== ''))
|
||||
]
|
||||
optionsCache.set(key, unique)
|
||||
setOptions(unique)
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
@ -85,7 +113,9 @@ const SimplePropertyFilter = ({
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [modelType, propertyName, getModelPropertyValues, property])
|
||||
// Intentionally omit getModelPropertyValues — context recreates it on every
|
||||
// provider render (e.g. table fetchLoading), which would refetch endlessly.
|
||||
}, [modelType, propertyName, property])
|
||||
|
||||
const valueKey = useMemo(
|
||||
() => JSON.stringify((value || []).map(getOptionKey)),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user