Add QuickPropertyFilters Component and Enhance FilterInput and ObjectTable
- Introduced QuickPropertyFilters component for streamlined property filtering functionality. - Updated FilterInput to integrate QuickPropertyFilters, enhancing user experience with quick access to filter options. - Modified ObjectTable to include QuickPropertyFilters, improving filtering capabilities within the dashboard. - Adjusted styles in App.css for dropdown z-index and input font consistency.
This commit is contained in:
parent
1597370a16
commit
3352c25819
@ -47,6 +47,10 @@
|
|||||||
font-family: 'DM Sans';
|
font-family: 'DM Sans';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-dropdown {
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
.ant-typography code,
|
.ant-typography code,
|
||||||
.ant-typography pre,
|
.ant-typography pre,
|
||||||
.ͼ1 .cm-scroller {
|
.ͼ1 .cm-scroller {
|
||||||
@ -793,7 +797,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input-number-cal .ant-input {
|
.input-number-cal .ant-input {
|
||||||
font-family: 'DM Mono';
|
font-family: 'DM Sans';
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { useCompactItemContext } from 'antd/es/space/Compact'
|
|||||||
import useStyle, { useSharedStyle } from 'antd/es/input/style'
|
import useStyle, { useSharedStyle } from 'antd/es/input/style'
|
||||||
import { useThemeContext } from '../context/ThemeContext'
|
import { useThemeContext } from '../context/ThemeContext'
|
||||||
import SimplePropertyFilter from './SimplePropertyFilter'
|
import SimplePropertyFilter from './SimplePropertyFilter'
|
||||||
|
import QuickPropertyFilters from './QuickPropertyFilters'
|
||||||
import ScrollBox from './ScrollBox'
|
import ScrollBox from './ScrollBox'
|
||||||
import ChevronRightIcon from '../../Icons/ChevronRightIcon'
|
import ChevronRightIcon from '../../Icons/ChevronRightIcon'
|
||||||
import GreaterThanIcon from '../../Icons/GreaterThanIcon'
|
import GreaterThanIcon from '../../Icons/GreaterThanIcon'
|
||||||
@ -25,9 +26,12 @@ import LessThanIcon from '../../Icons/LessThanIcon'
|
|||||||
import LessThanOrEqualToIcon from '../../Icons/LessThanOrEqualToIcon'
|
import LessThanOrEqualToIcon from '../../Icons/LessThanOrEqualToIcon'
|
||||||
import NotEqualIcon from '../../Icons/NotEqualIcon'
|
import NotEqualIcon from '../../Icons/NotEqualIcon'
|
||||||
import EqualIcon from '../../Icons/EqualIcon'
|
import EqualIcon from '../../Icons/EqualIcon'
|
||||||
|
import { Divider } from 'antd'
|
||||||
|
|
||||||
const operandIconStyle = { fontSize: 8 }
|
const operandIconStyle = { fontSize: 8 }
|
||||||
|
|
||||||
|
const PROPERTY_FILTER_PANEL_MAX_HEIGHT = 220
|
||||||
|
|
||||||
// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">".
|
// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">".
|
||||||
// Wildcards (* ?) are highlighted in place; @ stays as plain text — none are operands.
|
// Wildcards (* ?) are highlighted in place; @ stays as plain text — none are operands.
|
||||||
const WILDCARDS = new Set(['*', '?'])
|
const WILDCARDS = new Set(['*', '?'])
|
||||||
@ -469,8 +473,12 @@ const FilterInput = ({
|
|||||||
const composingRef = useRef(false)
|
const composingRef = useRef(false)
|
||||||
const selectingRef = useRef(false)
|
const selectingRef = useRef(false)
|
||||||
const valueRef = useRef(value ?? '')
|
const valueRef = useRef(value ?? '')
|
||||||
|
const quickFiltersRef = useRef(null)
|
||||||
|
const quickFilterModalOpenRef = useRef(false)
|
||||||
const [focused, setFocused] = useState(false)
|
const [focused, setFocused] = useState(false)
|
||||||
const [internalValue, setInternalValue] = useState(value ?? '')
|
const [internalValue, setInternalValue] = useState(value ?? '')
|
||||||
|
const [quickFiltersHeight, setQuickFiltersHeight] = useState(0)
|
||||||
|
const [quickFilterModalOpen, setQuickFilterModalOpen] = useState(false)
|
||||||
|
|
||||||
const clearTagRoots = useCallback(() => {
|
const clearTagRoots = useCallback(() => {
|
||||||
tagRootsRef.current.forEach((root) => {
|
tagRootsRef.current.forEach((root) => {
|
||||||
@ -864,6 +872,48 @@ const FilterInput = ({
|
|||||||
[emitChange, paint]
|
[emitChange, paint]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleQuickFilterModalOpenChange = useCallback((open) => {
|
||||||
|
quickFilterModalOpenRef.current = open
|
||||||
|
setQuickFilterModalOpen(open)
|
||||||
|
if (open) {
|
||||||
|
editorRef.current?.blur()
|
||||||
|
focusedRef.current = false
|
||||||
|
setFocused(false)
|
||||||
|
} else {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
editorRef.current?.focus()
|
||||||
|
focusedRef.current = true
|
||||||
|
setFocused(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const propertyFilterPopoverOpen = focused || quickFilterModalOpen
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!propertyFilterEnabled) {
|
||||||
|
setQuickFiltersHeight(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = quickFiltersRef.current
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
setQuickFiltersHeight(node.getBoundingClientRect().height)
|
||||||
|
}
|
||||||
|
|
||||||
|
update()
|
||||||
|
const observer = new ResizeObserver(update)
|
||||||
|
observer.observe(node)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [
|
||||||
|
propertyFilterEnabled,
|
||||||
|
propertyFilter?.modelType,
|
||||||
|
propertyFilter?.propertyName,
|
||||||
|
focused
|
||||||
|
])
|
||||||
|
|
||||||
const propertyFilterContent = propertyFilterEnabled ? (
|
const propertyFilterContent = propertyFilterEnabled ? (
|
||||||
<div
|
<div
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
@ -872,17 +922,33 @@ const FilterInput = ({
|
|||||||
}}
|
}}
|
||||||
className='filter-input-property-filter'
|
className='filter-input-property-filter'
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-block',
|
display: 'inline-flex',
|
||||||
|
flexDirection: 'column',
|
||||||
maxWidth: 280,
|
maxWidth: 280,
|
||||||
maxHeight: 220,
|
maxHeight: quickFiltersHeight + PROPERTY_FILTER_PANEL_MAX_HEIGHT,
|
||||||
margin: -4,
|
margin: -4,
|
||||||
verticalAlign: 'top'
|
verticalAlign: 'top',
|
||||||
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<div ref={quickFiltersRef} style={{ flexShrink: 0 }}>
|
||||||
|
<QuickPropertyFilters
|
||||||
|
modelType={propertyFilter.modelType}
|
||||||
|
propertyName={propertyFilter.propertyName}
|
||||||
|
onChange={handlePropertyFilterChange}
|
||||||
|
onModalOpenChange={handleQuickFilterModalOpenChange}
|
||||||
|
useCard={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Divider style={{ margin: '4px 0' }} />
|
||||||
<ScrollBox
|
<ScrollBox
|
||||||
inner
|
inner
|
||||||
smallPadding
|
smallPadding
|
||||||
style={{ height: 'auto', maxHeight: 220, maxWidth: 280 }}
|
style={{
|
||||||
|
flexShrink: 0,
|
||||||
|
maxHeight: PROPERTY_FILTER_PANEL_MAX_HEIGHT,
|
||||||
|
maxWidth: 280
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@ -1031,7 +1097,9 @@ const FilterInput = ({
|
|||||||
zIndex: focused ? 3 : style?.zIndex
|
zIndex: focused ? 3 : style?.zIndex
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!disabled) editorRef.current?.focus()
|
if (!disabled && !quickFilterModalOpenRef.current) {
|
||||||
|
editorRef.current?.focus()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<style>{`
|
<style>{`
|
||||||
@ -1100,6 +1168,11 @@ const FilterInput = ({
|
|||||||
onFocus?.(event)
|
onFocus?.(event)
|
||||||
}}
|
}}
|
||||||
onBlur={(event) => {
|
onBlur={(event) => {
|
||||||
|
if (quickFilterModalOpenRef.current) {
|
||||||
|
focusedRef.current = false
|
||||||
|
setFocused(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
focusedRef.current = false
|
focusedRef.current = false
|
||||||
selectingRef.current = false
|
selectingRef.current = false
|
||||||
setFocused(false)
|
setFocused(false)
|
||||||
@ -1151,9 +1224,9 @@ const FilterInput = ({
|
|||||||
)}
|
)}
|
||||||
{propertyFilterEnabled && (
|
{propertyFilterEnabled && (
|
||||||
<Popover
|
<Popover
|
||||||
open={focused}
|
open={propertyFilterPopoverOpen}
|
||||||
destroyOnHidden={true}
|
destroyOnHidden={true}
|
||||||
content={focused ? propertyFilterContent : null}
|
content={propertyFilterPopoverOpen ? propertyFilterContent : null}
|
||||||
placement='bottomLeft'
|
placement='bottomLeft'
|
||||||
arrow={false}
|
arrow={false}
|
||||||
trigger={[]}
|
trigger={[]}
|
||||||
|
|||||||
@ -49,6 +49,7 @@ import ActionsIcon from '../../Icons/ActionsIcon'
|
|||||||
import FilterIcon from '../../Icons/FilterIcon'
|
import FilterIcon from '../../Icons/FilterIcon'
|
||||||
import ScrollBox from './ScrollBox'
|
import ScrollBox from './ScrollBox'
|
||||||
import SimplePropertyFilter from './SimplePropertyFilter'
|
import SimplePropertyFilter from './SimplePropertyFilter'
|
||||||
|
import QuickPropertyFilters from './QuickPropertyFilters'
|
||||||
import FilterInput from './FilterInput'
|
import FilterInput from './FilterInput'
|
||||||
import {
|
import {
|
||||||
getActiveFilterValues,
|
getActiveFilterValues,
|
||||||
@ -159,6 +160,11 @@ const ColumnFilterDropdown = ({
|
|||||||
<Button onClick={resetFilter} icon={<XMarkIcon />} />
|
<Button onClick={resetFilter} icon={<XMarkIcon />} />
|
||||||
<Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
|
<Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
|
<QuickPropertyFilters
|
||||||
|
modelType={modelType}
|
||||||
|
propertyName={propertyName}
|
||||||
|
onChange={handleDraftChange}
|
||||||
|
/>
|
||||||
<Card size='small' styles={{ body: { padding: 0, height: 200 } }}>
|
<Card size='small' styles={{ body: { padding: 0, height: 200 } }}>
|
||||||
<ScrollBox inner={true} smallPadding={true}>
|
<ScrollBox inner={true} smallPadding={true}>
|
||||||
<div style={{ padding: '18px 20px', minWidth: 0 }}>
|
<div style={{ padding: '18px 20px', minWidth: 0 }}>
|
||||||
|
|||||||
443
src/components/Dashboard/common/QuickPropertyFilters.jsx
Normal file
443
src/components/Dashboard/common/QuickPropertyFilters.jsx
Normal file
@ -0,0 +1,443 @@
|
|||||||
|
import { useMemo, useRef, useState } from 'react'
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
DatePicker,
|
||||||
|
Descriptions,
|
||||||
|
Flex,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
Typography
|
||||||
|
} from 'antd'
|
||||||
|
import PropTypes from 'prop-types'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import InfoCircleIcon from '../../Icons/InfoCircleIcon.jsx'
|
||||||
|
import InputNumberCal from './InputNumberCal'
|
||||||
|
import {
|
||||||
|
getModelByName,
|
||||||
|
getModelProperties
|
||||||
|
} from '../../../database/ObjectModels'
|
||||||
|
import { isDateTimePropertyType } from './filterExpression'
|
||||||
|
|
||||||
|
const { Text } = Typography
|
||||||
|
|
||||||
|
const TEXT_TYPES = new Set([
|
||||||
|
'text',
|
||||||
|
'email',
|
||||||
|
'url',
|
||||||
|
'codeBlock',
|
||||||
|
'markdown',
|
||||||
|
'tags',
|
||||||
|
'stringList',
|
||||||
|
'country',
|
||||||
|
'phone',
|
||||||
|
'address',
|
||||||
|
'secret',
|
||||||
|
'miscId'
|
||||||
|
])
|
||||||
|
|
||||||
|
const NUMBER_TYPES = new Set(['number', 'density', 'variance', 'numberList'])
|
||||||
|
|
||||||
|
const BOOL_TYPES = new Set(['bool', 'boolean'])
|
||||||
|
|
||||||
|
const OBJECT_TYPES = new Set([
|
||||||
|
'object',
|
||||||
|
'objectList',
|
||||||
|
'reference',
|
||||||
|
'state',
|
||||||
|
'id',
|
||||||
|
'objectType'
|
||||||
|
])
|
||||||
|
|
||||||
|
const formatFilterValue = (value, property) => {
|
||||||
|
if (value == null || value === '') return ''
|
||||||
|
if (typeof value !== 'object') return String(value)
|
||||||
|
|
||||||
|
if (property?.type === 'state') {
|
||||||
|
return String(value.type ?? value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.objectType) {
|
||||||
|
const { prefix } = getModelByName(value.objectType)
|
||||||
|
if (value._reference != null) return `${prefix}:${value._reference}`
|
||||||
|
if (value._id != null) return `${prefix}:${value._id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property?.objectType && value._id != null) {
|
||||||
|
const { prefix } = getModelByName(property.objectType)
|
||||||
|
return `${prefix}:${value._id}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value._id ?? value.type ?? JSON.stringify(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDateFilterValue = (value, property) => {
|
||||||
|
if (value == null || value === '') return ''
|
||||||
|
const date = dayjs(value)
|
||||||
|
if (!date.isValid()) return String(value)
|
||||||
|
if (property?.type === 'date') {
|
||||||
|
return `${date.date()} ${date.month() + 1} ${date.year()}`
|
||||||
|
}
|
||||||
|
return date.toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
const buildExpression = (operator, rawValue, property) => {
|
||||||
|
const value = isDateTimePropertyType(property)
|
||||||
|
? formatDateFilterValue(rawValue, property)
|
||||||
|
: formatFilterValue(rawValue, property)
|
||||||
|
|
||||||
|
switch (operator) {
|
||||||
|
case 'startsWith':
|
||||||
|
return `${value}*`
|
||||||
|
case 'notStartsWith':
|
||||||
|
return `<>${value}*`
|
||||||
|
case 'contains':
|
||||||
|
return `*${value}*`
|
||||||
|
case 'notContains':
|
||||||
|
return `<>*${value}*`
|
||||||
|
case 'endsWith':
|
||||||
|
return `*${value}`
|
||||||
|
case 'notEndsWith':
|
||||||
|
return `<>*${value}`
|
||||||
|
case 'equals':
|
||||||
|
return value
|
||||||
|
case 'notEquals':
|
||||||
|
return `<>${value}`
|
||||||
|
case 'greaterThan':
|
||||||
|
return `>${value}`
|
||||||
|
case 'greaterOrEqual':
|
||||||
|
return `>=${value}`
|
||||||
|
case 'lessThan':
|
||||||
|
return `<${value}`
|
||||||
|
case 'lessOrEqual':
|
||||||
|
return `<=${value}`
|
||||||
|
case 'between':
|
||||||
|
return value
|
||||||
|
case 'isTrue':
|
||||||
|
return 'true'
|
||||||
|
case 'isFalse':
|
||||||
|
return 'false'
|
||||||
|
case 'isEmpty':
|
||||||
|
return ''
|
||||||
|
default:
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPropertyCategory = (property) => {
|
||||||
|
const type = property?.type
|
||||||
|
if (!type) return 'text'
|
||||||
|
if (BOOL_TYPES.has(type)) return 'boolean'
|
||||||
|
if (isDateTimePropertyType(property)) return 'date'
|
||||||
|
if (NUMBER_TYPES.has(type)) return 'number'
|
||||||
|
if (OBJECT_TYPES.has(type)) return 'object'
|
||||||
|
if (TEXT_TYPES.has(type)) return 'text'
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFilterOptions = (category) => {
|
||||||
|
switch (category) {
|
||||||
|
case 'boolean':
|
||||||
|
return [
|
||||||
|
{ key: 'isTrue', label: 'Is true' },
|
||||||
|
{ key: 'isFalse', label: 'Is false' }
|
||||||
|
]
|
||||||
|
case 'number':
|
||||||
|
return [
|
||||||
|
{ key: 'equals', label: 'Equals', needsValue: true },
|
||||||
|
{ key: 'notEquals', label: 'Does not equal', needsValue: true },
|
||||||
|
{ key: 'greaterThan', label: 'Greater than', needsValue: true },
|
||||||
|
{
|
||||||
|
key: 'greaterOrEqual',
|
||||||
|
label: 'Greater than or equal',
|
||||||
|
needsValue: true
|
||||||
|
},
|
||||||
|
{ key: 'lessThan', label: 'Less than', needsValue: true },
|
||||||
|
{
|
||||||
|
key: 'lessOrEqual',
|
||||||
|
label: 'Less than or equal',
|
||||||
|
needsValue: true
|
||||||
|
},
|
||||||
|
{ key: 'between', label: 'Between', needsRange: true }
|
||||||
|
]
|
||||||
|
case 'date':
|
||||||
|
return [
|
||||||
|
{ key: 'equals', label: 'On', needsValue: true },
|
||||||
|
{ key: 'lessThan', label: 'Before', needsValue: true },
|
||||||
|
{ key: 'greaterThan', label: 'After', needsValue: true },
|
||||||
|
{ key: 'between', label: 'Between', needsRange: true }
|
||||||
|
]
|
||||||
|
case 'object':
|
||||||
|
return [
|
||||||
|
{ key: 'equals', label: 'Equals', needsValue: true },
|
||||||
|
{ key: 'notEquals', label: 'Does not equal', needsValue: true }
|
||||||
|
]
|
||||||
|
case 'text':
|
||||||
|
default:
|
||||||
|
return [
|
||||||
|
{ key: 'startsWith', label: 'Starts with', needsValue: true },
|
||||||
|
{
|
||||||
|
key: 'notStartsWith',
|
||||||
|
label: 'Does not start with',
|
||||||
|
needsValue: true
|
||||||
|
},
|
||||||
|
{ key: 'contains', label: 'Contains', needsValue: true },
|
||||||
|
{ key: 'notContains', label: 'Does not contain', needsValue: true },
|
||||||
|
{ key: 'endsWith', label: 'Ends with', needsValue: true },
|
||||||
|
{ key: 'notEndsWith', label: 'Does not end with', needsValue: true },
|
||||||
|
{ key: 'equals', label: 'Equals', needsValue: true },
|
||||||
|
{ key: 'notEquals', label: 'Does not equal', needsValue: true },
|
||||||
|
{ key: 'isEmpty', label: 'Is empty' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuickPropertyFilters = ({
|
||||||
|
modelType,
|
||||||
|
propertyName,
|
||||||
|
onChange,
|
||||||
|
onModalOpenChange,
|
||||||
|
useCard = true
|
||||||
|
}) => {
|
||||||
|
const [activeOption, setActiveOption] = useState(null)
|
||||||
|
const [inputValue, setInputValue] = useState(null)
|
||||||
|
const [rangeFrom, setRangeFrom] = useState(null)
|
||||||
|
const [rangeTo, setRangeTo] = useState(null)
|
||||||
|
const modalContentRef = useRef(null)
|
||||||
|
|
||||||
|
const property = useMemo(
|
||||||
|
() =>
|
||||||
|
getModelProperties(modelType).find((prop) => prop.name === propertyName),
|
||||||
|
[modelType, propertyName]
|
||||||
|
)
|
||||||
|
|
||||||
|
const category = useMemo(() => getPropertyCategory(property), [property])
|
||||||
|
|
||||||
|
const options = useMemo(() => getFilterOptions(category), [category])
|
||||||
|
|
||||||
|
const propertyLabel = property?.label || propertyName
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
onModalOpenChange?.(false)
|
||||||
|
setActiveOption(null)
|
||||||
|
setInputValue(null)
|
||||||
|
setRangeFrom(null)
|
||||||
|
setRangeTo(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyFilter = (operator, rawValue) => {
|
||||||
|
const expression = buildExpression(operator, rawValue, property)
|
||||||
|
onChange?.([expression])
|
||||||
|
closeModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOptionClick = (option) => {
|
||||||
|
if (option.key === 'isTrue' || option.key === 'isFalse') {
|
||||||
|
applyFilter(option.key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (option.key === 'isEmpty') {
|
||||||
|
applyFilter('isEmpty')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setInputValue(null)
|
||||||
|
setRangeFrom(null)
|
||||||
|
setRangeTo(null)
|
||||||
|
onModalOpenChange?.(true)
|
||||||
|
setActiveOption(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleModalOk = () => {
|
||||||
|
if (!activeOption) return
|
||||||
|
|
||||||
|
if (activeOption.needsRange) {
|
||||||
|
const from = isDateTimePropertyType(property)
|
||||||
|
? formatDateFilterValue(rangeFrom, property)
|
||||||
|
: formatFilterValue(rangeFrom, property)
|
||||||
|
const to = isDateTimePropertyType(property)
|
||||||
|
? formatDateFilterValue(rangeTo, property)
|
||||||
|
: formatFilterValue(rangeTo, property)
|
||||||
|
|
||||||
|
if (!from && !to) return
|
||||||
|
applyFilter('between', `${from}..${to}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
inputValue == null ||
|
||||||
|
(typeof inputValue === 'string' && inputValue.trim() === '')
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
applyFilter(activeOption.key, inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const focusFirstModalInput = () => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
modalContentRef.current
|
||||||
|
?.querySelector('input, textarea, .ant-picker-input input')
|
||||||
|
?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderValueInput = (value, onValueChange) => {
|
||||||
|
if (isDateTimePropertyType(property)) {
|
||||||
|
const pickerValue =
|
||||||
|
value == null ? null : dayjs.isDayjs(value) ? value : dayjs(value)
|
||||||
|
const validValue =
|
||||||
|
pickerValue && pickerValue.isValid() ? pickerValue : null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DatePicker
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
showTime={property?.type === 'dateTime'}
|
||||||
|
value={validValue}
|
||||||
|
onChange={onValueChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NUMBER_TYPES.has(property?.type)) {
|
||||||
|
return (
|
||||||
|
<InputNumberCal
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
value={value}
|
||||||
|
onChange={onValueChange}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type='text'
|
||||||
|
value={value ?? ''}
|
||||||
|
onChange={(event) => onValueChange(event.target.value)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!property) return null
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Flex vertical gap={0}>
|
||||||
|
{options.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.key}
|
||||||
|
type='text'
|
||||||
|
block
|
||||||
|
style={{
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
textAlign: 'left',
|
||||||
|
padding: '5px 12px'
|
||||||
|
}}
|
||||||
|
onClick={() => handleOptionClick(option)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{useCard ? (
|
||||||
|
<Card size='small' styles={{ body: { padding: '4px 4px' } }}>
|
||||||
|
{content}
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
)}
|
||||||
|
<Modal
|
||||||
|
open={activeOption != null}
|
||||||
|
onCancel={closeModal}
|
||||||
|
afterOpenChange={(open) => {
|
||||||
|
if (open) focusFirstModalInput()
|
||||||
|
}}
|
||||||
|
destroyOnHidden
|
||||||
|
focusTriggerAfterClose={false}
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
closeIcon={null}
|
||||||
|
getContainer={() => document.body}
|
||||||
|
width={520}
|
||||||
|
>
|
||||||
|
{activeOption && (
|
||||||
|
<Flex
|
||||||
|
ref={modalContentRef}
|
||||||
|
vertical
|
||||||
|
gap='middle'
|
||||||
|
onMouseDown={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Flex gap='middle'>
|
||||||
|
<InfoCircleIcon />
|
||||||
|
<Text strong>Filter by {propertyLabel}</Text>
|
||||||
|
</Flex>
|
||||||
|
<Text>
|
||||||
|
{activeOption.needsRange
|
||||||
|
? `Enter a range to continue:`
|
||||||
|
: `Enter a value to continue:`}
|
||||||
|
</Text>
|
||||||
|
{activeOption.needsRange ? (
|
||||||
|
<Flex vertical gap='middle'>
|
||||||
|
<Descriptions
|
||||||
|
column={1}
|
||||||
|
size='small'
|
||||||
|
styles={{ label: { width: '60px' } }}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'from',
|
||||||
|
label: (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
justify='center'
|
||||||
|
>
|
||||||
|
From
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
children: renderValueInput(rangeFrom, setRangeFrom)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'to',
|
||||||
|
label: (
|
||||||
|
<Flex
|
||||||
|
vertical
|
||||||
|
style={{ height: '100%' }}
|
||||||
|
justify='center'
|
||||||
|
>
|
||||||
|
To
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
children: renderValueInput(rangeTo, setRangeTo)
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
) : activeOption.needsValue ? (
|
||||||
|
renderValueInput(inputValue, setInputValue)
|
||||||
|
) : null}
|
||||||
|
<Flex justify='end' gap='small'>
|
||||||
|
<Button type='default' onClick={closeModal}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type='primary' onClick={handleModalOk}>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
QuickPropertyFilters.propTypes = {
|
||||||
|
modelType: PropTypes.string,
|
||||||
|
propertyName: PropTypes.string,
|
||||||
|
onChange: PropTypes.func,
|
||||||
|
onModalOpenChange: PropTypes.func,
|
||||||
|
useCard: PropTypes.bool
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QuickPropertyFilters
|
||||||
Loading…
x
Reference in New Issue
Block a user