Compare commits
No commits in common. "6b848d392cb0b793eb7f33182940ba344a6838d7" and "3ed92a141b11b538b7f23fd6acca94b745c51139" have entirely different histories.
6b848d392c
...
3ed92a141b
@ -1,990 +0,0 @@
|
|||||||
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 (
|
|
||||||
<ConfigProvider theme={themeValue}>
|
|
||||||
<Tag
|
|
||||||
color={operand?.color ?? 'default'}
|
|
||||||
bordered={false}
|
|
||||||
style={{
|
|
||||||
marginInlineEnd: 0,
|
|
||||||
marginRight: 0,
|
|
||||||
height: '18px',
|
|
||||||
cursor: 'default',
|
|
||||||
padding: '0 4px',
|
|
||||||
marginTop: '-2px',
|
|
||||||
lineHeight: 1.37,
|
|
||||||
userSelect: 'none',
|
|
||||||
pointerEvents: 'none',
|
|
||||||
flexShrink: 0,
|
|
||||||
fontWeight: 600
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{operand?.label ?? symbol}
|
|
||||||
</Tag>
|
|
||||||
</ConfigProvider>
|
|
||||||
)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
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(
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
affixCls,
|
|
||||||
{
|
|
||||||
[`${affixCls}-focused`]: focused,
|
|
||||||
[`${affixCls}-disabled`]: disabled,
|
|
||||||
[`${affixCls}-sm`]: mergedSize === 'small',
|
|
||||||
[`${affixCls}-lg`]: mergedSize === 'large',
|
|
||||||
[`${affixCls}-rtl`]: direction === 'rtl',
|
|
||||||
[`${prefixCls}-outlined`]: true
|
|
||||||
},
|
|
||||||
compactItemClassnames,
|
|
||||||
cssVarCls,
|
|
||||||
rootCls,
|
|
||||||
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'
|
|
||||||
}}
|
|
||||||
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={{
|
|
||||||
// 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>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 }
|
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
|
import { useMemo, useState, useEffect, useRef, useCallback } from 'react'
|
||||||
import { Card, Select, Button, Flex, Space, Dropdown } from 'antd'
|
import { Card, Select, Input, Button, Flex, Space, Dropdown } from 'antd'
|
||||||
import FilterInput from './FilterInput'
|
|
||||||
import { CloseOutlined } from '@ant-design/icons'
|
import { CloseOutlined } from '@ant-design/icons'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import PlusIcon from '../../Icons/PlusIcon'
|
import PlusIcon from '../../Icons/PlusIcon'
|
||||||
@ -162,10 +161,10 @@ const FilterSidebar = ({
|
|||||||
style={{ minWidth: 80 }}
|
style={{ minWidth: 80 }}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
/>
|
/>
|
||||||
<FilterInput
|
<Input
|
||||||
placeholder='Value'
|
placeholder='Value'
|
||||||
value={row.value}
|
value={row.value}
|
||||||
onChange={(value) => changeValue(row.field, value)}
|
onChange={(e) => changeValue(row.field, e.target.value)}
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Spin,
|
Spin,
|
||||||
Button,
|
Button,
|
||||||
|
Input,
|
||||||
Space,
|
Space,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Form,
|
Form,
|
||||||
@ -50,7 +51,6 @@ 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 FilterInput from './FilterInput'
|
|
||||||
import {
|
import {
|
||||||
getActiveFilterValues,
|
getActiveFilterValues,
|
||||||
useTableStatePersistence
|
useTableStatePersistence
|
||||||
@ -117,7 +117,8 @@ const ColumnFilterDropdown = ({
|
|||||||
setExpression(toFilterExpression(next) ?? '')
|
setExpression(toFilterExpression(next) ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExpressionChange = (text) => {
|
const handleExpressionChange = (e) => {
|
||||||
|
const text = e.target.value
|
||||||
setExpression(text)
|
setExpression(text)
|
||||||
setDraft(fromFilterExpression(text) || [])
|
setDraft(fromFilterExpression(text) || [])
|
||||||
}
|
}
|
||||||
@ -145,12 +146,12 @@ const ColumnFilterDropdown = ({
|
|||||||
<div style={{ padding: 8 }}>
|
<div style={{ padding: 8 }}>
|
||||||
<Flex vertical gap='small'>
|
<Flex vertical gap='small'>
|
||||||
<Space.Compact>
|
<Space.Compact>
|
||||||
<FilterInput
|
<Input
|
||||||
placeholder={'Filter ' + propertyLabel}
|
placeholder={'Filter ' + propertyLabel}
|
||||||
value={expression}
|
value={expression}
|
||||||
onChange={handleExpressionChange}
|
onChange={handleExpressionChange}
|
||||||
onPressEnter={applyFilter}
|
onPressEnter={applyFilter}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200, display: 'block' }}
|
||||||
/>
|
/>
|
||||||
<Button onClick={resetFilter} icon={<XMarkIcon />} />
|
<Button onClick={resetFilter} icon={<XMarkIcon />} />
|
||||||
<Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
|
<Button type='primary' onClick={applyFilter} icon={<CheckIcon />} />
|
||||||
|
|||||||
@ -3,8 +3,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
useContext,
|
useContext,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect
|
||||||
useEffectEvent
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
import { notification, Drawer } from 'antd'
|
import { notification, Drawer } from 'antd'
|
||||||
@ -20,7 +19,7 @@ const NotificationContext = createContext()
|
|||||||
const NotificationProvider = ({ children }) => {
|
const NotificationProvider = ({ children }) => {
|
||||||
const [api, contextHolder] = notification.useNotification()
|
const [api, contextHolder] = notification.useNotification()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { authenticated } = useContext(AuthContext)
|
const { authenticated, token } = useContext(AuthContext)
|
||||||
const {
|
const {
|
||||||
showError,
|
showError,
|
||||||
fetchNotificationsApi,
|
fetchNotificationsApi,
|
||||||
@ -28,8 +27,7 @@ const NotificationProvider = ({ children }) => {
|
|||||||
markAllNotificationsAsReadApi,
|
markAllNotificationsAsReadApi,
|
||||||
deleteNotificationApi,
|
deleteNotificationApi,
|
||||||
deleteAllNotificationsApi,
|
deleteAllNotificationsApi,
|
||||||
registerNotificationListener,
|
registerNotificationListener
|
||||||
connected
|
|
||||||
} = useContext(ApiServerContext)
|
} = useContext(ApiServerContext)
|
||||||
|
|
||||||
const [notificationCenterVisible, setNotificationCenterVisible] =
|
const [notificationCenterVisible, setNotificationCenterVisible] =
|
||||||
@ -60,11 +58,6 @@ const NotificationProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [authenticated, fetchNotificationsApi, showError])
|
}, [authenticated, fetchNotificationsApi, showError])
|
||||||
|
|
||||||
const onFetchNotifications = useEffectEvent(() => {
|
|
||||||
if (!authenticated || !connected) return
|
|
||||||
fetchNotifications()
|
|
||||||
})
|
|
||||||
|
|
||||||
const markNotificationAsRead = useCallback(
|
const markNotificationAsRead = useCallback(
|
||||||
async (notificationId) => {
|
async (notificationId) => {
|
||||||
try {
|
try {
|
||||||
@ -119,17 +112,17 @@ const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
const unreadCount = notifications.filter((n) => !n.read).length
|
const unreadCount = notifications.filter((n) => !n.read).length
|
||||||
|
|
||||||
// Initial load / when we become authenticated and connected
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onFetchNotifications()
|
if (authenticated == true && notificationCenterVisible == true) {
|
||||||
}, [authenticated, connected])
|
fetchNotifications()
|
||||||
|
|
||||||
// Refresh when the notification center opens
|
|
||||||
useEffect(() => {
|
|
||||||
if (notificationCenterVisible) {
|
|
||||||
onFetchNotifications()
|
|
||||||
}
|
}
|
||||||
}, [notificationCenterVisible])
|
}, [authenticated, notificationCenterVisible, fetchNotifications])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (authenticated && token) {
|
||||||
|
fetchNotifications()
|
||||||
|
}
|
||||||
|
}, [authenticated, token, fetchNotifications])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNotificationCenterVisible(false)
|
setNotificationCenterVisible(false)
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export const Courier = {
|
|||||||
content: () => createElement(CourierInfo)
|
content: () => createElement(CourierInfo)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['_reference', 'name', 'country', 'email', 'website', 'createdAt', 'updatedAt'],
|
columns: ['_reference', 'name', 'country', 'email', 'website', 'createdAt'],
|
||||||
filters: [
|
filters: [
|
||||||
'name',
|
'name',
|
||||||
'website',
|
'website',
|
||||||
|
|||||||
@ -82,9 +82,7 @@ export const CourierService = {
|
|||||||
'deliveryTime',
|
'deliveryTime',
|
||||||
'cost',
|
'cost',
|
||||||
'costWithTax',
|
'costWithTax',
|
||||||
'active',
|
'active'
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
'_id',
|
'_id',
|
||||||
|
|||||||
@ -71,7 +71,6 @@ export const DocumentPrinter = {
|
|||||||
'host',
|
'host',
|
||||||
'tags',
|
'tags',
|
||||||
'connectedAt',
|
'connectedAt',
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { createElement, lazy } from 'react'
|
import { createElement, lazy } from 'react'
|
||||||
|
|
||||||
const DocumentTemplateInfo = lazy(
|
const DocumentTemplateInfo = lazy(
|
||||||
() =>
|
() => import('../../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo')
|
||||||
import('../../components/Dashboard/Management/DocumentTemplates/DocumentTemplateInfo')
|
|
||||||
)
|
)
|
||||||
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
import InfoCircleIcon from '../../components/Icons/InfoCircleIcon'
|
||||||
import EditIcon from '../../components/Icons/EditIcon'
|
import EditIcon from '../../components/Icons/EditIcon'
|
||||||
@ -66,7 +65,7 @@ export const DocumentTemplate = {
|
|||||||
visible: (objectData) => {
|
visible: (objectData) => {
|
||||||
return objectData?._isEditing && objectData?._isEditing == true
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
@ -91,8 +90,6 @@ export const DocumentTemplate = {
|
|||||||
'tags',
|
'tags',
|
||||||
'active',
|
'active',
|
||||||
'global',
|
'global',
|
||||||
'parent',
|
|
||||||
'documentSize',
|
|
||||||
'objectType',
|
'objectType',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export const FilamentProfile = {
|
|||||||
content: () => createElement(FilamentProfileInfo)
|
content: () => createElement(FilamentProfileInfo)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['_reference', 'name', 'filamentType', 'filament', 'createdAt', 'updatedAt'],
|
columns: ['_reference', 'name', 'filamentType', 'filament', 'updatedAt'],
|
||||||
filters: ['_id','name','createdAt','updatedAt','_reference'],
|
filters: ['_id','name','createdAt','updatedAt','_reference'],
|
||||||
sorters: ['name', 'createdAt', 'updatedAt'],
|
sorters: ['name', 'createdAt', 'updatedAt'],
|
||||||
properties: [
|
properties: [
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export const GCodeFile = {
|
|||||||
visible: (objectData) => {
|
visible: (objectData) => {
|
||||||
return objectData?._isEditing && objectData?._isEditing == true
|
return objectData?._isEditing && objectData?._isEditing == true
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
pages: [
|
pages: [
|
||||||
{
|
{
|
||||||
@ -94,7 +94,6 @@ export const GCodeFile = {
|
|||||||
'gcodeFileInfo.sparseInfillPattern',
|
'gcodeFileInfo.sparseInfillPattern',
|
||||||
'gcodeFileInfo.nozzleTemperature',
|
'gcodeFileInfo.nozzleTemperature',
|
||||||
'gcodeFileInfo.hotPlateTemp',
|
'gcodeFileInfo.hotPlateTemp',
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
|
|||||||
@ -90,26 +90,23 @@ export const Host = {
|
|||||||
content: () => createElement(HostInfo)
|
content: () => createElement(HostInfo)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
columns: ['_reference', 'name', 'state', 'tags', 'connectedAt'],
|
||||||
'_reference',
|
|
||||||
'name',
|
|
||||||
'state',
|
|
||||||
'tags',
|
|
||||||
'connectedAt',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
|
||||||
],
|
|
||||||
filters: [
|
filters: [
|
||||||
'name',
|
'name',
|
||||||
'_id',
|
'_id',
|
||||||
'state',
|
'state',
|
||||||
'tags',
|
'tags',
|
||||||
'connectedAt',
|
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
'_reference'
|
'_reference'
|
||||||
],
|
],
|
||||||
sorters: ['name', 'state', 'connectedAt', 'createdAt', 'updatedAt'],
|
sorters: [
|
||||||
|
'name',
|
||||||
|
'state',
|
||||||
|
'connectedAt',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt'
|
||||||
|
],
|
||||||
group: ['tags'],
|
group: ['tags'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -44,29 +44,6 @@ export const Note = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: [
|
|
||||||
'_reference',
|
|
||||||
'user',
|
|
||||||
'parentType',
|
|
||||||
'parent',
|
|
||||||
'noteType',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
|
||||||
],
|
|
||||||
filters: [
|
|
||||||
'_id',
|
|
||||||
'user',
|
|
||||||
'user._id',
|
|
||||||
'parentType',
|
|
||||||
'parent',
|
|
||||||
'parent._id',
|
|
||||||
'noteType',
|
|
||||||
'noteType._id',
|
|
||||||
'createdAt',
|
|
||||||
'updatedAt',
|
|
||||||
'_reference'
|
|
||||||
],
|
|
||||||
sorters: ['createdAt', 'updatedAt', 'parentType', 'noteType'],
|
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: '_id',
|
name: '_id',
|
||||||
|
|||||||
@ -292,12 +292,11 @@ export const Printer = {
|
|||||||
'host',
|
'host',
|
||||||
'tags',
|
'tags',
|
||||||
'connectedAt',
|
'connectedAt',
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
'tags',
|
'tags',
|
||||||
'host',
|
'host._id',
|
||||||
'active',
|
'active',
|
||||||
'online',
|
'online',
|
||||||
'name',
|
'name',
|
||||||
@ -307,7 +306,7 @@ export const Printer = {
|
|||||||
'createdAt',
|
'createdAt',
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
sorters: ['name', 'state', 'connectedAt', 'createdAt', 'updatedAt', 'host'],
|
sorters: ['name', 'state', 'connectedAt', 'createdAt', 'updatedAt'],
|
||||||
group: ['tags'],
|
group: ['tags'],
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -352,7 +352,6 @@ export const PrinterProfile = {
|
|||||||
'zOffset',
|
'zOffset',
|
||||||
'scanFirstLayer',
|
'scanFirstLayer',
|
||||||
'auxiliaryFan',
|
'auxiliaryFan',
|
||||||
'createdAt',
|
|
||||||
'updatedAt'
|
'updatedAt'
|
||||||
],
|
],
|
||||||
filters: ['_id','name','createdAt','updatedAt','_reference'],
|
filters: ['_id','name','createdAt','updatedAt','_reference'],
|
||||||
|
|||||||
@ -50,8 +50,7 @@ export const SubJob = {
|
|||||||
'state',
|
'state',
|
||||||
'startedAt',
|
'startedAt',
|
||||||
'finishedAt',
|
'finishedAt',
|
||||||
'createdAt',
|
'createdAt'
|
||||||
'updatedAt'
|
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
'_id',
|
'_id',
|
||||||
|
|||||||
@ -81,8 +81,7 @@ export const TaxRate = {
|
|||||||
'rateType',
|
'rateType',
|
||||||
'active',
|
'active',
|
||||||
'country',
|
'country',
|
||||||
'createdAt',
|
'createdAt'
|
||||||
'updatedAt'
|
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
'name',
|
'name',
|
||||||
|
|||||||
@ -82,8 +82,7 @@ export const TaxRecord = {
|
|||||||
'amount',
|
'amount',
|
||||||
'taxAmount',
|
'taxAmount',
|
||||||
'transactionDate',
|
'transactionDate',
|
||||||
'createdAt',
|
'createdAt'
|
||||||
'updatedAt'
|
|
||||||
],
|
],
|
||||||
filters: [
|
filters: [
|
||||||
'taxRate',
|
'taxRate',
|
||||||
|
|||||||
@ -87,7 +87,7 @@ export const User = {
|
|||||||
content: () => createElement(UserInfo)
|
content: () => createElement(UserInfo)
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
columns: ['_reference', 'name', 'username', 'email', 'role', 'createdAt', 'updatedAt'],
|
columns: ['_reference', 'name', 'username', 'email', 'role', 'createdAt'],
|
||||||
filters: [
|
filters: [
|
||||||
'name',
|
'name',
|
||||||
'username',
|
'username',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user