diff --git a/src/components/Dashboard/common/FilterInput.jsx b/src/components/Dashboard/common/FilterInput.jsx new file mode 100644 index 0000000..9e2aaa0 --- /dev/null +++ b/src/components/Dashboard/common/FilterInput.jsx @@ -0,0 +1,990 @@ +import { useCallback, useContext, useEffect, useRef, useState } from 'react' +import { ConfigProvider, Tag, theme } from 'antd' +import { CloseCircleFilled } from '@ant-design/icons' +import PropTypes from 'prop-types' +import { createRoot } from 'react-dom/client' +import classNames from 'classnames' +import useCSSVarCls from 'antd/es/config-provider/hooks/useCSSVarCls' +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' + +// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">". +// Wildcards (* ?) and @ stay as plain text — they are not operands. +const OPERANDS = [ + { symbol: '..', label: 'TO', color: 'cyan' }, + { symbol: '<>', label: '≠', color: 'volcano' }, + { symbol: '>=', label: '> OR =', color: 'orange' }, + { symbol: '<=', label: '< OR =', color: 'orange' }, + { symbol: '|', label: 'OR', color: 'purple' }, + { symbol: '&', label: 'AND', color: 'purple' }, + { symbol: '>', label: '>', color: 'orange' }, + { symbol: '<', label: '<', color: 'orange' }, + { symbol: '=', label: '=', color: 'blue' } +] + +const OPERAND_BY_SYMBOL = Object.fromEntries( + OPERANDS.map((operand) => [operand.symbol, operand]) +) + +const tokenize = (raw = '') => { + const value = String(raw) + const tokens = [] + let text = '' + let i = 0 + + while (i < value.length) { + const match = OPERANDS.find((operand) => + value.startsWith(operand.symbol, i) + ) + if (match) { + if (text) { + tokens.push({ type: 'text', value: text }) + text = '' + } + tokens.push({ type: 'operand', value: match.symbol }) + i += match.symbol.length + } else { + text += value[i] + i += 1 + } + } + + if (text) tokens.push({ type: 'text', value: text }) + return tokens +} + +const normalize = (raw = '') => + tokenize(raw) + .map((token) => token.value) + .join('') + +const nodeLength = (node) => { + if (!node) return 0 + if (node.nodeType === Node.TEXT_NODE) return node.textContent?.length ?? 0 + const operand = node.getAttribute?.('data-operand') + if (operand != null) return operand.length + return Array.from(node.childNodes).reduce( + (sum, child) => sum + nodeLength(child), + 0 + ) +} + +const serializeNode = (root) => { + if (!root) return '' + let result = '' + for (const node of root.childNodes) { + if (node.nodeType === Node.TEXT_NODE) { + result += node.textContent ?? '' + } else if (node.getAttribute?.('data-operand') != null) { + result += node.getAttribute('data-operand') + } else { + result += serializeNode(node) + } + } + return result +} + +/** Character offset in the serialized filter string for a DOM point. */ +const getOffsetAt = (root, targetNode, targetOffset, chipSnap = 'end') => { + if (!root || !targetNode || !root.contains(targetNode)) { + return serializeNode(root).length + } + + let offset = 0 + + const visit = (node) => { + if (node === targetNode) { + if (node.nodeType === Node.TEXT_NODE) { + offset += targetOffset + } else if (node.getAttribute?.('data-operand') != null) { + offset += targetOffset <= 0 ? 0 : node.getAttribute('data-operand').length + } else { + for (let i = 0; i < targetOffset; i += 1) { + offset += nodeLength(node.childNodes[i]) + } + } + return true + } + + if (node.nodeType === Node.TEXT_NODE) { + offset += node.textContent?.length ?? 0 + return false + } + + if (node.getAttribute?.('data-operand') != null) { + if (node.contains(targetNode)) { + // Chips are atomic: snap range starts to the chip start, ends to the chip end. + if (chipSnap !== 'start') { + offset += node.getAttribute('data-operand').length + } + return true + } + offset += node.getAttribute('data-operand').length + return false + } + + for (const child of node.childNodes) { + if (visit(child)) return true + } + return false + } + + visit(root) + return offset +} + +/** Character offset of the selection focus (caret). */ +const getCaretOffset = (root) => { + const selection = window.getSelection() + if (!selection?.rangeCount || !root.contains(selection.focusNode)) { + return serializeNode(root).length + } + return getOffsetAt(root, selection.focusNode, selection.focusOffset, 'end') +} + +/** Serialized filter text for the current selection (operand symbols, not labels). */ +const getSelectedSerializedText = (root) => { + const selection = window.getSelection() + if (!selection?.rangeCount || selection.isCollapsed) return '' + + const range = selection.getRangeAt(0) + if ( + !root.contains(range.startContainer) || + !root.contains(range.endContainer) + ) { + return null + } + + const start = getOffsetAt( + root, + range.startContainer, + range.startOffset, + 'start' + ) + const end = getOffsetAt(root, range.endContainer, range.endOffset, 'end') + return serializeNode(root).slice(Math.min(start, end), Math.max(start, end)) +} + +/** Keep the caret / selection focus visible inside a horizontally clipped editor. */ +const scrollPointIntoView = (root, clientX) => { + if (!root || clientX == null || Number.isNaN(clientX)) return + const rect = root.getBoundingClientRect() + const pad = 8 + if (clientX > rect.right - pad) { + root.scrollLeft += clientX - (rect.right - pad) + } else if (clientX < rect.left + pad) { + root.scrollLeft -= rect.left + pad - clientX + } +} + +const scrollSelectionIntoView = (root) => { + if (!root) return + const selection = window.getSelection() + if (!selection?.rangeCount || !root.contains(selection.focusNode)) return + + try { + const range = document.createRange() + range.setStart(selection.focusNode, selection.focusOffset) + range.collapse(true) + const rects = range.getClientRects() + const rect = rects[rects.length - 1] ?? range.getBoundingClientRect() + if (rect && (rect.width > 0 || rect.height > 0)) { + scrollPointIntoView(root, rect.left) + return + } + } catch { + // Fall through to caret-at-end heuristic. + } + + // Empty / collapsed-at-end selections often have a zero rect — scroll to end. + if (getCaretOffset(root) >= serializeNode(root).length) { + root.scrollLeft = root.scrollWidth + } +} + +const caretRangeFromPoint = (clientX, clientY) => { + if (document.caretRangeFromPoint) { + return document.caretRangeFromPoint(clientX, clientY) + } + if (document.caretPositionFromPoint) { + const pos = document.caretPositionFromPoint(clientX, clientY) + if (!pos) return null + const range = document.createRange() + range.setStart(pos.offsetNode, pos.offset) + range.collapse(true) + return range + } + return null +} + +/** Extend the live selection to the DOM point under the pointer (used while edge-scrolling). */ +const extendSelectionToPoint = (root, clientX, clientY) => { + const selection = window.getSelection() + if (!selection?.rangeCount || !root) return + const caret = caretRangeFromPoint(clientX, clientY) + if (!caret || !root.contains(caret.startContainer)) return + try { + selection.setBaseAndExtent( + selection.anchorNode, + selection.anchorOffset, + caret.startContainer, + caret.startOffset + ) + } catch { + // Ignore invalid selection boundaries (e.g. inside non-editable chips). + } +} + +const setCaretOffset = (root, targetOffset) => { + if (!root) return + const selection = window.getSelection() + if (!selection) return + + let remaining = Math.max(0, targetOffset) + + const place = (node, offset) => { + const range = document.createRange() + range.setStart(node, offset) + range.collapse(true) + selection.removeAllRanges() + selection.addRange(range) + scrollSelectionIntoView(root) + } + + for (let i = 0; i < root.childNodes.length; i += 1) { + const child = root.childNodes[i] + const length = nodeLength(child) + + if (child.nodeType === Node.TEXT_NODE) { + if (remaining <= length) { + place(child, remaining) + return + } + remaining -= length + continue + } + + if (child.getAttribute?.('data-operand') != null) { + if (remaining === 0) { + place(root, i) + return + } + if (remaining <= length) { + place(root, i + 1) + return + } + remaining -= length + } + } + + const range = document.createRange() + range.selectNodeContents(root) + range.collapse(false) + selection.removeAllRanges() + selection.addRange(range) + scrollSelectionIntoView(root) +} + +/** True when the DOM already mirrors tokenize(serialized). */ +const domMatchesTokens = (root, raw) => { + const tokens = tokenize(raw) + const children = Array.from(root.childNodes).filter((node) => { + if (node.nodeType === Node.TEXT_NODE) return (node.textContent ?? '') !== '' + return node.getAttribute?.('data-operand') != null + }) + + // Empty editor may keep a single empty text node. + if (tokens.length === 0) { + return ( + root.childNodes.length === 0 || + (root.childNodes.length === 1 && + root.childNodes[0].nodeType === Node.TEXT_NODE && + (root.childNodes[0].textContent ?? '') === '') + ) + } + + if (children.length !== tokens.length) return false + + return tokens.every((token, index) => { + const child = children[index] + if (token.type === 'text') { + return ( + child.nodeType === Node.TEXT_NODE && child.textContent === token.value + ) + } + return child.getAttribute?.('data-operand') === token.value + }) +} + +/** Operand chips are only "live" when their portal React root is still mounted. */ +const chipsHaveLiveRoots = (root, roots) => { + for (const node of root.childNodes) { + if (node.getAttribute?.('data-operand') == null) continue + if (!roots.has(node)) return false + } + return true +} + +const FilterInput = ({ + value = '', + onChange, + placeholder = 'Filter…', + disabled = false, + allowClear = false, + className = '', + style, + onFocus, + onBlur, + onPressEnter, + size +}) => { + const { getPrefixCls, direction } = useContext(ConfigProvider.ConfigContext) + const prefixCls = getPrefixCls('input') + const { token } = theme.useToken() + const { themeConfig } = useThemeContext() + const themeConfigRef = useRef(themeConfig) + themeConfigRef.current = themeConfig + + const rootCls = useCSSVarCls(prefixCls) + const [wrapSharedCSSVar, hashId, cssVarCls] = useSharedStyle(prefixCls) + const [wrapCSSVar] = useStyle(prefixCls, rootCls) + + const { compactSize, compactItemClassnames } = useCompactItemContext( + prefixCls, + direction + ) + const mergedSize = useSize((ctx) => size ?? compactSize ?? ctx) + + const editorRef = useRef(null) + const tagRootsRef = useRef(new Map()) + const focusedRef = useRef(false) + const composingRef = useRef(false) + const selectingRef = useRef(false) + const valueRef = useRef(value ?? '') + const [focused, setFocused] = useState(false) + const [internalValue, setInternalValue] = useState(value ?? '') + + const clearTagRoots = useCallback(() => { + tagRootsRef.current.forEach((root) => { + queueMicrotask(() => root.unmount()) + }) + tagRootsRef.current.clear() + }, []) + + const unmountChip = useCallback((wrapper) => { + const root = tagRootsRef.current.get(wrapper) + if (root) { + queueMicrotask(() => root.unmount()) + tagRootsRef.current.delete(wrapper) + } + }, []) + + const renderTagNode = useCallback((symbol, themeValue) => { + const operand = OPERAND_BY_SYMBOL[symbol] + return ( + + + {operand?.label ?? symbol} + + + ) + }, []) + + const renderOperandChip = useCallback( + (symbol) => { + const wrapper = document.createElement('span') + wrapper.setAttribute('data-operand', symbol) + wrapper.setAttribute('contenteditable', 'false') + wrapper.style.display = 'inline-flex' + wrapper.style.alignItems = 'center' + wrapper.style.verticalAlign = 'middle' + wrapper.style.margin = '0 2px' + wrapper.style.userSelect = 'none' + wrapper.style.flexShrink = '0' + + const root = createRoot(wrapper) + root.render(renderTagNode(symbol, themeConfigRef.current)) + tagRootsRef.current.set(wrapper, root) + return wrapper + }, + [renderTagNode] + ) + + // Portal Tags sit outside the React tree — re-render them when theme changes. + useEffect(() => { + tagRootsRef.current.forEach((root, wrapper) => { + root.render( + renderTagNode(wrapper.getAttribute('data-operand'), themeConfig) + ) + }) + }, [themeConfig, renderTagNode]) + + const emitChange = useCallback( + (next) => { + valueRef.current = next + setInternalValue(next) + onChange?.(next) + }, + [onChange] + ) + + const paint = useCallback( + (raw, caret = null) => { + const editor = editorRef.current + if (!editor) return + + const normalized = normalize(raw) + + // Already in sync with live chip roots — skip to avoid a tag flash. + if ( + domMatchesTokens(editor, normalized) && + chipsHaveLiveRoots(editor, tagRootsRef.current) + ) { + if (caret != null && focusedRef.current) { + requestAnimationFrame(() => { + setCaretOffset(editor, caret) + scrollSelectionIntoView(editor) + }) + } + return + } + + const scrollLeft = editor.scrollLeft + const tokens = tokenize(normalized) + + // Reuse operand chips that still have a mounted React root. + const chipPool = new Map() + for (const node of Array.from(editor.childNodes)) { + const symbol = node.getAttribute?.('data-operand') + if (symbol == null) continue + if (!tagRootsRef.current.has(node)) continue + const list = chipPool.get(symbol) + if (list) list.push(node) + else chipPool.set(symbol, [node]) + } + + const takeChip = (symbol) => { + const list = chipPool.get(symbol) + while (list?.length) { + const wrapper = list.shift() + if (tagRootsRef.current.has(wrapper)) return wrapper + } + return renderOperandChip(symbol) + } + + const nextChildren = [] + if (tokens.length === 0) { + nextChildren.push(document.createTextNode('')) + } else { + for (const item of tokens) { + if (item.type === 'text') { + nextChildren.push(document.createTextNode(item.value)) + } else { + nextChildren.push(takeChip(item.value)) + } + } + } + + chipPool.forEach((list) => { + list.forEach((wrapper) => unmountChip(wrapper)) + }) + + editor.replaceChildren(...nextChildren) + editor.scrollLeft = scrollLeft + + if (caret != null && focusedRef.current) { + requestAnimationFrame(() => { + setCaretOffset(editor, caret) + scrollSelectionIntoView(editor) + }) + } + }, + [renderOperandChip, unmountChip] + ) + + useEffect(() => { + const next = value ?? '' + valueRef.current = next + setInternalValue(next) + if (!focusedRef.current) { + paint(next) + } + }, [value, paint]) + + useEffect(() => () => clearTagRoots(), [clearTagRoots]) + + const syncFromDom = useCallback(() => { + const editor = editorRef.current + if (!editor) return + + const caret = getCaretOffset(editor) + const serialized = serializeNode(editor) + const normalized = normalize(serialized) + + if (!domMatchesTokens(editor, normalized)) { + paint(normalized, caret) + } else { + scrollSelectionIntoView(editor) + } + + if (normalized !== valueRef.current) { + emitChange(normalized) + } + }, [emitChange, paint]) + + const findOperandWrapper = (node) => { + const editor = editorRef.current + let current = node + while (current && current !== editor) { + if (current.getAttribute?.('data-operand') != null) return current + current = current.parentNode + } + return null + } + + const handleInput = () => { + if (composingRef.current || disabled) return + syncFromDom() + } + + const removeChip = (chip) => { + const editor = editorRef.current + if (!editor || !chip) return + const parent = chip.parentNode + const index = Array.from(parent.childNodes).indexOf(chip) + let offset = 0 + for (let i = 0; i < index; i += 1) { + offset += nodeLength(parent.childNodes[i]) + } + const root = tagRootsRef.current.get(chip) + if (root) { + queueMicrotask(() => root.unmount()) + tagRootsRef.current.delete(chip) + } + parent.removeChild(chip) + setCaretOffset(editor, offset) + syncFromDom() + } + + const chipBeforeCaret = () => { + const editor = editorRef.current + const selection = window.getSelection() + if (!editor || !selection?.rangeCount || !selection.isCollapsed) return null + const { startContainer, startOffset } = selection.getRangeAt(0) + + const insideOperand = findOperandWrapper(startContainer) + if (insideOperand) return insideOperand + + if (startContainer === editor && startOffset > 0) { + const prev = editor.childNodes[startOffset - 1] + if (prev?.getAttribute?.('data-operand') != null) return prev + } + if (startContainer.nodeType === Node.TEXT_NODE && startOffset === 0) { + const prev = startContainer.previousSibling + if (prev?.getAttribute?.('data-operand') != null) return prev + } + return null + } + + const chipAfterCaret = () => { + const editor = editorRef.current + const selection = window.getSelection() + if (!editor || !selection?.rangeCount || !selection.isCollapsed) return null + const { startContainer, startOffset } = selection.getRangeAt(0) + + const insideOperand = findOperandWrapper(startContainer) + if (insideOperand) return insideOperand + + if (startContainer === editor) { + const next = editor.childNodes[startOffset] + if (next?.getAttribute?.('data-operand') != null) return next + } + if ( + startContainer.nodeType === Node.TEXT_NODE && + startOffset === (startContainer.textContent?.length ?? 0) + ) { + const next = startContainer.nextSibling + if (next?.getAttribute?.('data-operand') != null) return next + } + return null + } + + const handleKeyDown = (event) => { + if (disabled) return + + if (event.key === 'Enter') { + event.preventDefault() + onPressEnter?.(internalValue) + return + } + + if (event.key === 'Backspace') { + const chip = chipBeforeCaret() + if (chip) { + event.preventDefault() + removeChip(chip) + } + return + } + + if (event.key === 'Delete') { + const chip = chipAfterCaret() + if (chip) { + event.preventDefault() + removeChip(chip) + } + } + } + + const handlePaste = (event) => { + if (disabled) return + event.preventDefault() + const text = (event.clipboardData?.getData('text/plain') ?? '').replace( + /[\r\n]+/g, + '' + ) + const editor = editorRef.current + if (!editor) return + + const selection = window.getSelection() + if (!selection?.rangeCount) return + + const range = selection.getRangeAt(0) + const start = getOffsetAt( + editor, + range.startContainer, + range.startOffset, + 'start' + ) + const end = getOffsetAt(editor, range.endContainer, range.endOffset, 'end') + const from = Math.min(start, end) + const to = Math.max(start, end) + const current = serializeNode(editor) + const next = `${current.slice(0, from)}${text}${current.slice(to)}` + const normalized = normalize(next) + paint(normalized, from + text.length) + emitChange(normalized) + } + + const handleCopy = (event) => { + const editor = editorRef.current + if (!editor) return + const selection = window.getSelection() + if (!selection?.rangeCount || selection.isCollapsed) return + const text = getSelectedSerializedText(editor) + if (text == null) return + event.preventDefault() + event.clipboardData?.setData('text/plain', text) + } + + const handleCut = (event) => { + if (disabled) return + const editor = editorRef.current + if (!editor) return + const selection = window.getSelection() + if (!selection?.rangeCount || selection.isCollapsed) return + + const text = getSelectedSerializedText(editor) + if (text == null) return + + event.preventDefault() + event.clipboardData?.setData('text/plain', text) + + const range = selection.getRangeAt(0) + const start = getOffsetAt( + editor, + range.startContainer, + range.startOffset, + 'start' + ) + const end = getOffsetAt(editor, range.endContainer, range.endOffset, 'end') + const from = Math.min(start, end) + const to = Math.max(start, end) + const current = serializeNode(editor) + const next = `${current.slice(0, from)}${current.slice(to)}` + const normalized = normalize(next) + paint(normalized, from) + emitChange(normalized) + } + + const handleClear = (event) => { + event.preventDefault() + event.stopPropagation() + if (disabled) return + paint('') + emitChange('') + editorRef.current?.focus() + } + + // Auto-scroll while click-dragging a selection past the visible edges. + useEffect(() => { + let raf = 0 + let lastClientX = null + + const stopRaf = () => { + if (raf) { + cancelAnimationFrame(raf) + raf = 0 + } + } + + const tick = () => { + raf = 0 + if (!selectingRef.current || !focusedRef.current || lastClientX == null) { + return + } + const editor = editorRef.current + if (!editor) return + + const rect = editor.getBoundingClientRect() + const edge = 28 + let dx = 0 + if (lastClientX >= rect.right - edge) { + dx = + Math.min(28, Math.ceil((lastClientX - (rect.right - edge)) / 2) + 6) + } else if (lastClientX <= rect.left + edge) { + dx = -Math.min( + 28, + Math.ceil((rect.left + edge - lastClientX) / 2) + 6 + ) + } + + if (dx !== 0) { + const maxScroll = editor.scrollWidth - editor.clientWidth + const next = Math.max(0, Math.min(maxScroll, editor.scrollLeft + dx)) + if (next !== editor.scrollLeft) { + editor.scrollLeft = next + extendSelectionToPoint( + editor, + lastClientX, + rect.top + rect.height / 2 + ) + scrollSelectionIntoView(editor) + raf = requestAnimationFrame(tick) + } + } + } + + const onMouseMove = (event) => { + if (!selectingRef.current || !focusedRef.current) return + lastClientX = event.clientX + const editor = editorRef.current + if (!editor) return + scrollPointIntoView(editor, event.clientX) + if (!raf) raf = requestAnimationFrame(tick) + } + + const onMouseUp = () => { + selectingRef.current = false + lastClientX = null + stopRaf() + } + + const onSelectionChange = () => { + if (!focusedRef.current) return + const editor = editorRef.current + if (!editor) return + const selection = window.getSelection() + if (!selection?.rangeCount || !editor.contains(selection.focusNode)) return + scrollSelectionIntoView(editor) + } + + document.addEventListener('mousemove', onMouseMove) + document.addEventListener('mouseup', onMouseUp) + document.addEventListener('selectionchange', onSelectionChange) + return () => { + stopRaf() + document.removeEventListener('mousemove', onMouseMove) + document.removeEventListener('mouseup', onMouseUp) + document.removeEventListener('selectionchange', onSelectionChange) + } + }, []) + + const showPlaceholder = !internalValue && !focused + const showClear = allowClear && !!internalValue && !disabled + const affixCls = `${prefixCls}-affix-wrapper` + const placeholderPaddingInline = + mergedSize === 'large' + ? (token.paddingInlineLG ?? token.paddingSM + 4) + : mergedSize === 'small' + ? (token.paddingInlineSM ?? token.paddingXS) + : (token.paddingInline ?? token.paddingSM) + + return wrapSharedCSSVar( + wrapCSSVar( + { + if (!disabled) editorRef.current?.focus() + }} + > + + {showPlaceholder && ( + + {placeholder} + + )} +
{ + selectingRef.current = true + }} + onCompositionStart={() => { + composingRef.current = true + }} + onCompositionEnd={() => { + composingRef.current = false + syncFromDom() + }} + onFocus={(event) => { + focusedRef.current = true + setFocused(true) + onFocus?.(event) + }} + onBlur={(event) => { + focusedRef.current = false + selectingRef.current = false + setFocused(false) + const next = normalize(serializeNode(editorRef.current)) + paint(next) + if (next !== valueRef.current) emitChange(next) + onBlur?.(event) + }} + style={{ + // Match antd's `> input.ant-input` resets inside affix-wrapper + display: 'block', + padding: 0, + border: 'none', + borderRadius: 0, + outline: 'none', + boxShadow: 'none', + background: 'transparent', + flex: 'auto', + minWidth: 0, + width: '100%', + fontSize: 'inherit', + lineHeight: 'inherit', + color: 'inherit', + whiteSpace: 'nowrap', + overflowX: 'auto', + overflowY: 'hidden', + scrollbarWidth: 'none', + msOverflowStyle: 'none', + wordBreak: 'keep-all', + overflowWrap: 'normal', + caretColor: token.colorText, + cursor: disabled ? 'not-allowed' : 'text' + }} + /> + {showClear && ( + + + + + + )} + + ) + ) +} + +FilterInput.propTypes = { + value: PropTypes.string, + onChange: PropTypes.func, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + allowClear: PropTypes.bool, + className: PropTypes.string, + style: PropTypes.object, + onFocus: PropTypes.func, + onBlur: PropTypes.func, + onPressEnter: PropTypes.func, + size: PropTypes.oneOf(['small', 'middle', 'large']) +} + +export default FilterInput +export { OPERANDS, tokenize } diff --git a/src/components/Dashboard/common/FilterSidebar.jsx b/src/components/Dashboard/common/FilterSidebar.jsx index ec7df4a..11ecb92 100644 --- a/src/components/Dashboard/common/FilterSidebar.jsx +++ b/src/components/Dashboard/common/FilterSidebar.jsx @@ -1,5 +1,6 @@ import { useMemo, useState, useEffect, useRef, useCallback } from 'react' -import { Card, Select, Input, Button, Flex, Space, Dropdown } from 'antd' +import { Card, Select, Button, Flex, Space, Dropdown } from 'antd' +import FilterInput from './FilterInput' import { CloseOutlined } from '@ant-design/icons' import PropTypes from 'prop-types' import PlusIcon from '../../Icons/PlusIcon' @@ -161,10 +162,10 @@ const FilterSidebar = ({ style={{ minWidth: 80 }} allowClear={false} /> - changeValue(row.field, e.target.value)} + onChange={(value) => changeValue(row.field, value)} style={{ flex: 1 }} />