Compare commits
No commits in common. "72c5ee1b913976c774c2d64c804fc917849306b6" and "6b848d392cb0b793eb7f33182940ba344a6838d7" have entirely different histories.
72c5ee1b91
...
6b848d392c
@ -1109,7 +1109,6 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
|
|
||||||
.simple-date-time-property-filter {
|
.simple-date-time-property-filter {
|
||||||
margin: -6px 0;
|
margin: -6px 0;
|
||||||
background-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.simple-date-time-property-filter.ant-tree .ant-tree-treenode {
|
.simple-date-time-property-filter.ant-tree .ant-tree-treenode {
|
||||||
@ -1131,11 +1130,3 @@ span.ant-skeleton-input.ant-skeleton-input-sm.text-skeleton {
|
|||||||
top: -16px !important;
|
top: -16px !important;
|
||||||
bottom: -24px !important;
|
bottom: -24px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Size FilterSidebar field selects to the longest label (via labelRender sizer). */
|
|
||||||
.filter-sidebar-field-select.ant-select {
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
.filter-sidebar-field-select .ant-select-selector {
|
|
||||||
width: max-content !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
import { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||||
import { ConfigProvider, Popover, Tag, theme } from 'antd'
|
import { ConfigProvider, Tag, theme } from 'antd'
|
||||||
import { CloseCircleFilled } from '@ant-design/icons'
|
import { CloseCircleFilled } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
@ -9,8 +9,6 @@ import useSize from 'antd/es/config-provider/hooks/useSize'
|
|||||||
import { useCompactItemContext } from 'antd/es/space/Compact'
|
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 ScrollBox from './ScrollBox'
|
|
||||||
|
|
||||||
// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">".
|
// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">".
|
||||||
// Wildcards (* ?) and @ stay as plain text — they are not operands.
|
// Wildcards (* ?) and @ stay as plain text — they are not operands.
|
||||||
@ -329,25 +327,6 @@ const chipsHaveLiveRoots = (root, roots) => {
|
|||||||
return true
|
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 = ({
|
const FilterInput = ({
|
||||||
value = '',
|
value = '',
|
||||||
onChange,
|
onChange,
|
||||||
@ -359,8 +338,7 @@ const FilterInput = ({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
onPressEnter,
|
onPressEnter,
|
||||||
size,
|
size
|
||||||
propertyFilter = null
|
|
||||||
}) => {
|
}) => {
|
||||||
const { getPrefixCls, direction } = useContext(ConfigProvider.ConfigContext)
|
const { getPrefixCls, direction } = useContext(ConfigProvider.ConfigContext)
|
||||||
const prefixCls = getPrefixCls('input')
|
const prefixCls = getPrefixCls('input')
|
||||||
@ -756,46 +734,6 @@ const FilterInput = ({
|
|||||||
editorRef.current?.focus()
|
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.
|
// Auto-scroll while click-dragging a selection past the visible edges.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let raf = 0
|
let raf = 0
|
||||||
@ -890,178 +828,148 @@ const FilterInput = ({
|
|||||||
? (token.paddingInlineSM ?? token.paddingXS)
|
? (token.paddingInlineSM ?? token.paddingXS)
|
||||||
: (token.paddingInline ?? token.paddingSM)
|
: (token.paddingInline ?? token.paddingSM)
|
||||||
|
|
||||||
const input = (
|
return wrapSharedCSSVar(
|
||||||
<span
|
wrapCSSVar(
|
||||||
className={classNames(
|
<span
|
||||||
affixCls,
|
className={classNames(
|
||||||
{
|
affixCls,
|
||||||
[`${affixCls}-focused`]: focused,
|
{
|
||||||
[`${affixCls}-disabled`]: disabled,
|
[`${affixCls}-focused`]: focused,
|
||||||
[`${affixCls}-sm`]: mergedSize === 'small',
|
[`${affixCls}-disabled`]: disabled,
|
||||||
[`${affixCls}-lg`]: mergedSize === 'large',
|
[`${affixCls}-sm`]: mergedSize === 'small',
|
||||||
[`${affixCls}-rtl`]: direction === 'rtl',
|
[`${affixCls}-lg`]: mergedSize === 'large',
|
||||||
[`${prefixCls}-outlined`]: true
|
[`${affixCls}-rtl`]: direction === 'rtl',
|
||||||
},
|
[`${prefixCls}-outlined`]: true
|
||||||
compactItemClassnames,
|
},
|
||||||
cssVarCls,
|
compactItemClassnames,
|
||||||
rootCls,
|
cssVarCls,
|
||||||
hashId,
|
rootCls,
|
||||||
className
|
hashId,
|
||||||
)}
|
className
|
||||||
style={{
|
)}
|
||||||
width: '100%',
|
|
||||||
cursor: disabled ? 'not-allowed' : 'text',
|
|
||||||
...style,
|
|
||||||
// Affix ::before strut needs inline-flex; display:block stacks it and inflates height.
|
|
||||||
display: 'inline-flex',
|
|
||||||
// Stay above Compact siblings so the focus ring isn't covered by Select/Button.
|
|
||||||
position: 'relative',
|
|
||||||
zIndex: focused ? 3 : style?.zIndex
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
if (!disabled) editorRef.current?.focus()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<style>{`
|
|
||||||
.filter-input-editor::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
{showPlaceholder && (
|
|
||||||
<span
|
|
||||||
aria-hidden
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
insetInlineStart: placeholderPaddingInline,
|
|
||||||
insetInlineEnd: showClear
|
|
||||||
? placeholderPaddingInline + 22
|
|
||||||
: placeholderPaddingInline,
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
color: token.colorTextPlaceholder,
|
|
||||||
pointerEvents: 'none',
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
zIndex: 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{placeholder}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
ref={editorRef}
|
|
||||||
className={classNames(prefixCls, 'filter-input-editor')}
|
|
||||||
role='textbox'
|
|
||||||
aria-multiline='false'
|
|
||||||
aria-disabled={disabled || undefined}
|
|
||||||
contentEditable={!disabled}
|
|
||||||
suppressContentEditableWarning
|
|
||||||
spellCheck={false}
|
|
||||||
onInput={handleInput}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
onPaste={handlePaste}
|
|
||||||
onCopy={handleCopy}
|
|
||||||
onCut={handleCut}
|
|
||||||
onMouseDown={() => {
|
|
||||||
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={{
|
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%',
|
width: '100%',
|
||||||
fontSize: 'inherit',
|
cursor: disabled ? 'not-allowed' : 'text',
|
||||||
lineHeight: 'inherit',
|
...style,
|
||||||
color: 'inherit',
|
// Affix ::before strut needs inline-flex; display:block stacks it and inflates height.
|
||||||
whiteSpace: 'nowrap',
|
display: 'inline-flex'
|
||||||
overflowX: 'auto',
|
|
||||||
overflowY: 'hidden',
|
|
||||||
scrollbarWidth: 'none',
|
|
||||||
msOverflowStyle: 'none',
|
|
||||||
wordBreak: 'keep-all',
|
|
||||||
overflowWrap: 'normal',
|
|
||||||
caretColor: token.colorText,
|
|
||||||
cursor: disabled ? 'not-allowed' : 'text'
|
|
||||||
}}
|
}}
|
||||||
/>
|
onClick={() => {
|
||||||
{showClear && (
|
if (!disabled) editorRef.current?.focus()
|
||||||
<span className={`${prefixCls}-suffix`}>
|
}}
|
||||||
<span
|
>
|
||||||
className={classNames(
|
<style>{`
|
||||||
`${prefixCls}-clear-icon`,
|
.filter-input-editor::-webkit-scrollbar {
|
||||||
`${prefixCls}-clear-icon-has-value`
|
display: none;
|
||||||
)}
|
height: 0;
|
||||||
role='button'
|
}
|
||||||
tabIndex={-1}
|
`}</style>
|
||||||
onMouseDown={handleClear}
|
{showPlaceholder && (
|
||||||
>
|
|
||||||
<CloseCircleFilled />
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{propertyFilterEnabled && (
|
|
||||||
<Popover
|
|
||||||
open={focused}
|
|
||||||
content={propertyFilterContent}
|
|
||||||
placement='bottomLeft'
|
|
||||||
arrow={false}
|
|
||||||
trigger={[]}
|
|
||||||
styles={{
|
|
||||||
body: {
|
|
||||||
padding: 8
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Anchor only — keep Popover from wrapping the Compact item (z-index/focus ring). */}
|
|
||||||
<span
|
<span
|
||||||
aria-hidden
|
aria-hidden
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
left: 0,
|
insetInlineStart: placeholderPaddingInline,
|
||||||
right: 0,
|
insetInlineEnd: showClear
|
||||||
|
? placeholderPaddingInline + 22
|
||||||
|
: placeholderPaddingInline,
|
||||||
|
top: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: 0,
|
display: 'flex',
|
||||||
pointerEvents: 'none'
|
alignItems: 'center',
|
||||||
|
color: token.colorTextPlaceholder,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
zIndex: 1
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
</Popover>
|
{placeholder}
|
||||||
)}
|
</span>
|
||||||
</span>
|
)}
|
||||||
|
<div
|
||||||
|
ref={editorRef}
|
||||||
|
className={classNames(prefixCls, 'filter-input-editor')}
|
||||||
|
role='textbox'
|
||||||
|
aria-multiline='false'
|
||||||
|
aria-disabled={disabled || undefined}
|
||||||
|
contentEditable={!disabled}
|
||||||
|
suppressContentEditableWarning
|
||||||
|
spellCheck={false}
|
||||||
|
onInput={handleInput}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onPaste={handlePaste}
|
||||||
|
onCopy={handleCopy}
|
||||||
|
onCut={handleCut}
|
||||||
|
onMouseDown={() => {
|
||||||
|
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 && (
|
||||||
|
<span className={`${prefixCls}-suffix`}>
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
`${prefixCls}-clear-icon`,
|
||||||
|
`${prefixCls}-clear-icon-has-value`
|
||||||
|
)}
|
||||||
|
role='button'
|
||||||
|
tabIndex={-1}
|
||||||
|
onMouseDown={handleClear}
|
||||||
|
>
|
||||||
|
<CloseCircleFilled />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return wrapSharedCSSVar(wrapCSSVar(input))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FilterInput.propTypes = {
|
FilterInput.propTypes = {
|
||||||
@ -1075,11 +983,7 @@ FilterInput.propTypes = {
|
|||||||
onFocus: PropTypes.func,
|
onFocus: PropTypes.func,
|
||||||
onBlur: PropTypes.func,
|
onBlur: PropTypes.func,
|
||||||
onPressEnter: 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
|
export default FilterInput
|
||||||
|
|||||||
@ -11,36 +11,6 @@ import {
|
|||||||
import MissingPlaceholder from './MissingPlaceholder'
|
import MissingPlaceholder from './MissingPlaceholder'
|
||||||
import BinIcon from '../../Icons/BinIcon'
|
import BinIcon from '../../Icons/BinIcon'
|
||||||
|
|
||||||
const hiddenSizerStyle = {
|
|
||||||
gridArea: '1 / 1',
|
|
||||||
visibility: 'hidden',
|
|
||||||
whiteSpace: 'nowrap'
|
|
||||||
}
|
|
||||||
|
|
||||||
const FieldSelectLabel = ({ label, options }) => (
|
|
||||||
<span style={{ display: 'inline-grid' }}>
|
|
||||||
<span aria-hidden style={hiddenSizerStyle}>
|
|
||||||
Field
|
|
||||||
</span>
|
|
||||||
{options.map((option) => (
|
|
||||||
<span key={option.value} aria-hidden style={hiddenSizerStyle}>
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<span style={{ gridArea: '1 / 1', whiteSpace: 'nowrap' }}>{label}</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
|
|
||||||
FieldSelectLabel.propTypes = {
|
|
||||||
label: PropTypes.node,
|
|
||||||
options: PropTypes.arrayOf(
|
|
||||||
PropTypes.shape({
|
|
||||||
label: PropTypes.node,
|
|
||||||
value: PropTypes.string
|
|
||||||
})
|
|
||||||
).isRequired
|
|
||||||
}
|
|
||||||
|
|
||||||
const FilterSidebar = ({
|
const FilterSidebar = ({
|
||||||
type,
|
type,
|
||||||
filter = {},
|
filter = {},
|
||||||
@ -69,17 +39,7 @@ const FilterSidebar = ({
|
|||||||
if (initialEmptyFields.current.has(k) && v === '') continue
|
if (initialEmptyFields.current.has(k) && v === '') continue
|
||||||
visible[k] = v
|
visible[k] = v
|
||||||
}
|
}
|
||||||
setLocalFilter((prev) => {
|
setLocalFilter(visible)
|
||||||
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])
|
}, [filter])
|
||||||
|
|
||||||
const debouncedFilterChange = useCallback(
|
const debouncedFilterChange = useCallback(
|
||||||
@ -199,22 +159,14 @@ const FilterSidebar = ({
|
|||||||
value={row.field || undefined}
|
value={row.field || undefined}
|
||||||
onChange={(v) => changeField(row.field, v)}
|
onChange={(v) => changeField(row.field, v)}
|
||||||
options={availableOptions(row.field)}
|
options={availableOptions(row.field)}
|
||||||
|
style={{ minWidth: 80 }}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
labelRender={({ label }) => (
|
|
||||||
<FieldSelectLabel label={label} options={fieldOptions} />
|
|
||||||
)}
|
|
||||||
style={{ flexShrink: 0 }}
|
|
||||||
classNames={{ root: 'filter-sidebar-field-select' }}
|
|
||||||
/>
|
/>
|
||||||
<FilterInput
|
<FilterInput
|
||||||
placeholder='Value'
|
placeholder='Value'
|
||||||
value={row.value}
|
value={row.value}
|
||||||
onChange={(value) => changeValue(row.field, value)}
|
onChange={(value) => changeValue(row.field, value)}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
propertyFilter={{
|
|
||||||
modelType: type,
|
|
||||||
propertyName: row.field
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<CloseOutlined />}
|
icon={<CloseOutlined />}
|
||||||
|
|||||||
@ -539,9 +539,7 @@ const SimpleDateTimePropertyFilter = ({
|
|||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
style={{ minWidth: 0 }}
|
style={{ minWidth: 0 }}
|
||||||
/>
|
/>
|
||||||
<Text style={{ minWidth: 0, paddingInlineStart: '8px' }}>
|
<Text style={{ minWidth: 0 }}>{node.title}</Text>
|
||||||
{node.title}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect, useContext, useMemo, useRef } from 'react'
|
import { useState, useEffect, useContext, useMemo } from 'react'
|
||||||
import { Flex, Checkbox, Spin, Typography } from 'antd'
|
import { Flex, Checkbox, Spin, Typography } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { ApiServerContext } from '../context/ApiServerContext'
|
import { ApiServerContext } from '../context/ApiServerContext'
|
||||||
@ -45,9 +45,6 @@ const matchOptions = (options, selected) => {
|
|||||||
return options.filter((option) => selectedKeys.has(getOptionKey(option)))
|
return options.filter((option) => selectedKeys.has(getOptionKey(option)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Survive table reloads / popover remounts without refetching the same property.
|
|
||||||
const optionsCache = new Map()
|
|
||||||
|
|
||||||
const SimplePropertyFilter = ({
|
const SimplePropertyFilter = ({
|
||||||
modelType,
|
modelType,
|
||||||
propertyName,
|
propertyName,
|
||||||
@ -56,17 +53,8 @@ const SimplePropertyFilter = ({
|
|||||||
search = ''
|
search = ''
|
||||||
}) => {
|
}) => {
|
||||||
const { getModelPropertyValues } = useContext(ApiServerContext)
|
const { getModelPropertyValues } = useContext(ApiServerContext)
|
||||||
const getModelPropertyValuesRef = useRef(getModelPropertyValues)
|
const [options, setOptions] = useState([])
|
||||||
getModelPropertyValuesRef.current = getModelPropertyValues
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
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 [localChecked, setLocalChecked] = useState(null)
|
||||||
|
|
||||||
const property = useMemo(
|
const property = useMemo(
|
||||||
@ -79,30 +67,14 @@ const SimplePropertyFilter = ({
|
|||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
const loadOptions = async () => {
|
const loadOptions = async () => {
|
||||||
if (!modelType || !propertyName || isDateTimeProperty(property)) {
|
if (!modelType || !propertyName || isDateTimeProperty(property)) return
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = `${modelType}:${propertyName}`
|
|
||||||
const cached = optionsCache.get(key)
|
|
||||||
if (cached) {
|
|
||||||
setOptions(cached)
|
|
||||||
setLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const values = await getModelPropertyValuesRef.current(
|
const values = await getModelPropertyValues(modelType, propertyName)
|
||||||
modelType,
|
|
||||||
propertyName
|
|
||||||
)
|
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
const unique = [
|
const unique = [
|
||||||
...new Set((values || []).filter((v) => v != null && v !== ''))
|
...new Set((values || []).filter((v) => v != null && v !== ''))
|
||||||
]
|
]
|
||||||
optionsCache.set(key, unique)
|
|
||||||
setOptions(unique)
|
setOptions(unique)
|
||||||
} finally {
|
} finally {
|
||||||
if (!cancelled) setLoading(false)
|
if (!cancelled) setLoading(false)
|
||||||
@ -113,9 +85,7 @@ const SimplePropertyFilter = ({
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
// Intentionally omit getModelPropertyValues — context recreates it on every
|
}, [modelType, propertyName, getModelPropertyValues, property])
|
||||||
// provider render (e.g. table fetchLoading), which would refetch endlessly.
|
|
||||||
}, [modelType, propertyName, property])
|
|
||||||
|
|
||||||
const valueKey = useMemo(
|
const valueKey = useMemo(
|
||||||
() => JSON.stringify((value || []).map(getOptionKey)),
|
() => JSON.stringify((value || []).map(getOptionKey)),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user