Add new comparison icons and update FilterInput with operand icons
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced new SVG icons for comparison operators: Equal, Greater Than, Greater Than or Equal To, Less Than, Less Than or Equal To, and Not Equal.
- Updated FilterInput component to utilize these new icons for better visual representation of operand symbols.
- Enhanced the layout and styling of operand labels within the FilterInput for improved user experience.
This commit is contained in:
Tom Butcher 2026-08-11 01:41:55 +01:00
parent a483fd91ca
commit fc2dbbd14d
13 changed files with 151 additions and 16 deletions

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.245638,0,0,1.245638,3,12.546644)">
<path d="M4.656,9.516L41.906,9.516C44.391,9.516 46.562,7.359 46.562,4.781C46.562,2.203 44.391,0.047 41.906,0.047L4.656,0.047C2.266,0.047 0,2.203 0,4.781C0,7.359 2.266,9.516 4.656,9.516ZM4.656,31.188L41.906,31.188C44.391,31.188 46.562,29.047 46.562,26.469C46.562,23.891 44.391,21.719 41.906,21.719L4.656,21.719C2.266,21.719 0,23.891 0,26.469C0,29.047 2.266,31.188 4.656,31.188Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 936 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(-1.243968,0,0,1.243968,61,3.835791)">
<path d="M42.109,45.25C44.844,45.25 46.625,43.016 46.625,40.172C46.625,37.656 45.609,36.141 42.891,35.062L13.141,22.688L13.141,22.297L42.875,9.891C45.422,8.859 46.625,7.391 46.625,4.984C46.625,2.156 44.656,0.031 41.844,0.031C40.516,0.031 39.406,0.359 38.281,0.906L5.203,15.984C1.922,17.453 0,19.469 0,22.688C0,25.938 1.922,27.969 5.203,29.453L38.281,44.281C39.5,44.828 40.688,45.25 42.109,45.25Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 956 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.039523,0,0,1.039523,10.860319,2)">
<path d="M0,52.969C0,55.75 1.984,57.719 4.734,57.719L35.953,57.719C38.688,57.719 40.672,55.75 40.672,52.969C40.672,50.219 38.688,48.25 35.953,48.25L4.734,48.25C1.984,48.25 0,50.219 0,52.969ZM4.531,40.031C1.766,40.031 0,37.797 0,34.953C0,32.453 1.094,30.984 3.734,29.906L26.438,19.969L26.438,19.625L3.75,9.844C1.266,8.828 0,7.359 0,4.953C0,2.125 1.969,0 4.797,0C6.109,0 7.297,0.328 8.344,0.859L34.375,13.328C37.609,14.891 39.578,16.766 39.578,19.984C39.578,23.25 37.625,25.125 34.375,26.703L8.344,39.094C7.188,39.672 5.938,40.031 4.531,40.031Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.243968,0,0,1.243968,3,3.835791)">
<path d="M42.109,45.25C44.844,45.25 46.625,43.016 46.625,40.172C46.625,37.656 45.609,36.141 42.891,35.062L13.141,22.688L13.141,22.297L42.875,9.891C45.422,8.859 46.625,7.391 46.625,4.984C46.625,2.156 44.656,0.031 41.844,0.031C40.516,0.031 39.406,0.359 38.281,0.906L5.203,15.984C1.922,17.453 0,19.469 0,22.688C0,25.938 1.922,27.969 5.203,29.453L38.281,44.281C39.5,44.828 40.688,45.25 42.109,45.25Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 954 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.039523,0,0,1.039523,10.860319,2)">
<path d="M0,52.969C0,55.75 1.984,57.719 4.734,57.719L35.953,57.719C38.688,57.719 40.672,55.75 40.672,52.969C40.672,50.219 38.688,48.25 35.953,48.25L4.734,48.25C1.984,48.25 0,50.219 0,52.969ZM35.047,40.031C37.812,40.031 39.578,37.797 39.578,34.953C39.578,32.453 38.484,30.984 35.844,29.906L13.141,19.969L13.141,19.625L35.828,9.844C38.312,8.828 39.578,7.359 39.578,4.953C39.578,2.125 37.609,0 34.781,0C33.469,0 32.281,0.328 31.234,0.859L5.203,13.328C1.969,14.891 0,16.766 0,19.984C0,23.25 1.953,25.125 5.203,26.703L31.234,39.094C32.391,39.672 33.641,40.031 35.047,40.031Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(1.245638,0,0,1.245638,3,12.546644)">
<path d="M4.656,9.516L41.906,9.516C44.391,9.516 46.562,7.359 46.562,4.781C46.562,2.203 44.391,0.047 41.906,0.047L4.656,0.047C2.266,0.047 0,2.203 0,4.781C0,7.359 2.266,9.516 4.656,9.516ZM4.656,31.188L41.906,31.188C44.391,31.188 46.562,29.047 46.562,26.469C46.562,23.891 44.391,21.719 41.906,21.719L4.656,21.719C2.266,21.719 0,23.891 0,26.469C0,29.047 2.266,31.188 4.656,31.188Z" style="fill-rule:nonzero;"/>
</g>
<g transform="matrix(1.245638,0,0,1.245638,3,-1.178629)">
<path d="M7.547,51.55C9.859,52.878 12.578,52.097 13.875,49.769L40.688,8.027C41.984,5.73 41.281,3.121 38.969,1.824C36.641,0.496 34.094,1.152 32.656,3.636L6,45.206C4.641,47.612 5.188,50.237 7.547,51.55ZM4.656,20.579L41.906,20.579C44.391,20.579 46.562,18.423 46.562,15.845C46.562,13.266 44.391,11.11 41.906,11.11L4.656,11.11C2.266,11.11 0,13.266 0,15.845C0,18.423 2.266,20.579 4.656,20.579ZM4.656,42.251L41.906,42.251C44.391,42.251 46.562,40.11 46.562,37.532C46.562,34.954 44.391,32.782 41.906,32.782L4.656,32.782C2.266,32.782 0,34.954 0,37.532C0,40.11 2.266,42.251 4.656,42.251Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,4 +1,11 @@
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState
} from 'react'
import { ConfigProvider, Popover, Tag, theme } from 'antd' import { ConfigProvider, Popover, 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'
@ -11,19 +18,58 @@ 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 ScrollBox from './ScrollBox' import ScrollBox from './ScrollBox'
import ArrowLeftIcon from '../../Icons/ArrowLeftIcon'
import GreaterThanIcon from '../../Icons/GreaterThanIcon'
import GreaterThanOrEqualToIcon from '../../Icons/GreaterThanOrEqualToIcon'
import LessThanIcon from '../../Icons/LessThanIcon'
import LessThanOrEqualToIcon from '../../Icons/LessThanOrEqualToIcon'
import NotEqualIcon from '../../Icons/NotEqualIcon'
import EqualIcon from '../../Icons/EqualIcon'
const operandIconStyle = { fontSize: 8 }
// 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.
const OPERANDS = [ const OPERANDS = [
{ symbol: '..', label: 'TO', color: 'cyan' }, {
{ symbol: '<>', label: '≠', color: 'volcano' }, symbol: '..',
{ symbol: '>=', label: '> OR =', color: 'orange' }, label: <ArrowLeftIcon style={operandIconStyle} />,
{ symbol: '<=', label: '< OR =', color: 'orange' }, color: 'cyan'
},
{
symbol: '<>',
label: <NotEqualIcon style={operandIconStyle} />,
color: 'volcano'
},
{
symbol: '>=',
label: <GreaterThanOrEqualToIcon style={operandIconStyle} />,
color: 'orange'
},
{
symbol: '<=',
label: <LessThanOrEqualToIcon style={operandIconStyle} />,
color: 'orange'
},
{ symbol: '|', label: 'OR', color: 'purple' }, { symbol: '|', label: 'OR', color: 'purple' },
{ symbol: '&', label: 'AND', color: 'purple' }, { symbol: '&', label: 'AND', color: 'purple' },
{ symbol: '>', label: '>', color: 'orange' }, {
{ symbol: '<', label: '<', color: 'orange' }, symbol: '>',
{ symbol: '=', label: '=', color: 'blue' } label: (
<GreaterThanIcon style={{ ...operandIconStyle, marginLeft: '1px' }} />
),
color: 'orange'
},
{
symbol: '<',
label: <LessThanIcon style={{ ...operandIconStyle, marginRight: '1px' }} />,
color: 'orange'
},
{
symbol: '=',
label: <EqualIcon style={operandIconStyle} />,
color: 'blue'
}
] ]
const OPERAND_BY_SYMBOL = Object.fromEntries( const OPERAND_BY_SYMBOL = Object.fromEntries(
@ -101,7 +147,8 @@ const getOffsetAt = (root, targetNode, targetOffset, chipSnap = 'end') => {
if (node.nodeType === Node.TEXT_NODE) { if (node.nodeType === Node.TEXT_NODE) {
offset += targetOffset offset += targetOffset
} else if (node.getAttribute?.('data-operand') != null) { } else if (node.getAttribute?.('data-operand') != null) {
offset += targetOffset <= 0 ? 0 : node.getAttribute('data-operand').length offset +=
targetOffset <= 0 ? 0 : node.getAttribute('data-operand').length
} else { } else {
for (let i = 0; i < targetOffset; i += 1) { for (let i = 0; i < targetOffset; i += 1) {
offset += nodeLength(node.childNodes[i]) offset += nodeLength(node.childNodes[i])
@ -421,7 +468,10 @@ const FilterInput = ({
userSelect: 'none', userSelect: 'none',
pointerEvents: 'none', pointerEvents: 'none',
flexShrink: 0, flexShrink: 0,
fontWeight: 600 fontWeight: 700,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center'
}} }}
> >
{operand?.label ?? symbol} {operand?.label ?? symbol}
@ -820,13 +870,12 @@ const FilterInput = ({
const edge = 28 const edge = 28
let dx = 0 let dx = 0
if (lastClientX >= rect.right - edge) { if (lastClientX >= rect.right - edge) {
dx = dx = Math.min(
Math.min(28, Math.ceil((lastClientX - (rect.right - edge)) / 2) + 6)
} else if (lastClientX <= rect.left + edge) {
dx = -Math.min(
28, 28,
Math.ceil((rect.left + edge - lastClientX) / 2) + 6 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) { if (dx !== 0) {
@ -865,7 +914,8 @@ const FilterInput = ({
const editor = editorRef.current const editor = editorRef.current
if (!editor) return if (!editor) return
const selection = window.getSelection() const selection = window.getSelection()
if (!selection?.rangeCount || !editor.contains(selection.focusNode)) return if (!selection?.rangeCount || !editor.contains(selection.focusNode))
return
scrollSelectionIntoView(editor) scrollSelectionIntoView(editor)
} }

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/equalicon.svg?react'
const EqualIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default EqualIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/greaterthanicon.svg?react'
const GreaterThanIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default GreaterThanIcon

View File

@ -0,0 +1,8 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/greaterthanorequaltoicon.svg?react'
const GreaterThanOrEqualToIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default GreaterThanOrEqualToIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/lessthanicon.svg?react'
const LessThanIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default LessThanIcon

View File

@ -0,0 +1,8 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/lessthanorequaltoicon.svg?react'
const LessThanOrEqualToIcon = (props) => (
<Icon component={CustomIconSvg} {...props} />
)
export default LessThanOrEqualToIcon

View File

@ -0,0 +1,6 @@
import Icon from '@ant-design/icons'
import CustomIconSvg from '../../../assets/icons/notequalicon.svg?react'
const NotEqualIcon = (props) => <Icon component={CustomIconSvg} {...props} />
export default NotEqualIcon