Enhance FilterInput and QuickPropertyFilters with Quote Support and Improved Options
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Added support for quote tokens in the FilterInput component, allowing for better expression handling.
- Updated QuickPropertyFilters to include new filter options for 'isEmpty' and 'isNotEmpty', enhancing filtering capabilities.
- Improved CSS styles for quote elements to ensure consistent appearance in light mode.
- Adjusted padding in ObjectTimelineItem for better layout consistency.
This commit is contained in:
Tom Butcher 2026-09-05 02:28:24 +01:00
parent 8438757db5
commit 56479ea358
4 changed files with 58 additions and 12 deletions

View File

@ -1780,6 +1780,11 @@ body.objectKanbanColumnResizing * {
box-shadow: -2px -1px 8px 4px rgba(253, 253, 253, 0.12);
}
.light-mode .objectTimelineShadow .objectTimelineRowLabel,
.light-mode .objectTimelineShadow .objectTimelineRowHeaderLabel {
box-shadow: -2px -1px 8px 4px rgba(5, 5, 5, 0.06);
}
.objectTimelineRow:hover,
.objectTimelineRow:hover .objectTimelineRowLabel {
background-color: var(--color-table-header-bg);
@ -2822,6 +2827,11 @@ body.objectKanbanColumnResizing * {
opacity: 0.5;
}
.filter-input-quote,
[data-quote] {
color: color-mix(in srgb, var(--color-text) 40%, transparent);
}
.filter-input-operand-tag {
margin-inline-end: 0;
margin-right: 0;

View File

@ -33,6 +33,7 @@ const PROPERTY_FILTER_PANEL_MAX_HEIGHT = 220
// Longer symbols first so ".." / "<>" / ">=" match before "." / "<" / ">".
// Wildcards (* ?) are highlighted in place; @ stays as plain text — none are operands.
const WILDCARDS = new Set(['*', '?'])
const QUOTE = "'"
const OPERANDS = [
{
@ -138,6 +139,11 @@ const tokenize = (raw = '') => {
text = ''
tokens.push({ type: 'wildcard', value: value[i] })
i += 1
} else if (value[i] === QUOTE) {
pushText(text)
text = ''
tokens.push({ type: 'quote', value: QUOTE })
i += 1
} else {
text += value[i]
i += 1
@ -164,6 +170,14 @@ const renderDateUnitNode = (value) => {
return span
}
const renderQuoteNode = () => {
const span = document.createElement('span')
span.setAttribute('data-quote', QUOTE)
span.className = 'filter-input-quote'
span.textContent = QUOTE
return span
}
const normalize = (raw = '') =>
tokenize(raw)
.map((token) => token.value)
@ -414,7 +428,8 @@ const domMatchesTokens = (root, raw) => {
return (
node.getAttribute?.('data-operand') != null ||
node.getAttribute?.('data-wildcard') != null ||
node.getAttribute?.('data-date-unit') != null
node.getAttribute?.('data-date-unit') != null ||
node.getAttribute?.('data-quote') != null
)
})
@ -449,6 +464,12 @@ const domMatchesTokens = (root, raw) => {
(child.textContent ?? '') === token.value
)
}
if (token.type === 'quote') {
return (
child.getAttribute?.('data-quote') === token.value &&
(child.textContent ?? '') === token.value
)
}
return child.getAttribute?.('data-operand') === token.value
})
}
@ -645,6 +666,8 @@ const FilterInput = ({
nextChildren.push(renderWildcardNode(item.value))
} else if (item.type === 'dateUnit') {
nextChildren.push(renderDateUnitNode(item.value))
} else if (item.type === 'quote') {
nextChildren.push(renderQuoteNode())
} else {
nextChildren.push(takeChip(item.value))
}

View File

@ -58,15 +58,13 @@ const ObjectTimelineItem = ({
minWidth: position.isPoint ? undefined : 28,
maxWidth: `calc(${100 - position.left}% - 4px)`,
height: 30,
padding: '0 10px',
padding: '0 11px',
overflow: 'visible',
whiteSpace: 'nowrap',
flex: '0 0 auto',
borderRadius: 8,
color: 'white',
background: color,
boxShadow:
'0 1px 3px color-mix(in srgb, var(--color-primary) 35%, transparent)'
background: color
}}
>
<div

View File

@ -120,7 +120,9 @@ const buildExpression = (operator, rawValue, property) => {
case 'isFalse':
return 'false'
case 'isEmpty':
return ''
return "''"
case 'isNotEmpty':
return "<>''"
default:
return value
}
@ -158,7 +160,10 @@ const getFilterOptions = (category) => {
case 'boolean':
return [
{ key: 'isTrue', label: 'Is true' },
{ key: 'isFalse', label: 'Is false' }
{ key: 'isFalse', label: 'Is false' },
{ type: 'divider' },
{ key: 'isEmpty', label: 'Is empty' },
{ key: 'isNotEmpty', label: 'Is not empty' }
]
case 'number':
return [
@ -178,7 +183,10 @@ const getFilterOptions = (category) => {
needsValue: true
},
{ type: 'divider' },
{ key: 'between', label: 'Between', needsRange: true }
{ key: 'between', label: 'Between', needsRange: true },
{ type: 'divider' },
{ key: 'isEmpty', label: 'Is empty' },
{ key: 'isNotEmpty', label: 'Is not empty' }
]
case 'date':
return [
@ -188,6 +196,9 @@ const getFilterOptions = (category) => {
{ key: 'greaterThan', label: 'After', needsValue: true },
{ key: 'between', label: 'Between', needsRange: true },
{ type: 'divider' },
{ key: 'isEmpty', label: 'Is empty' },
{ key: 'isNotEmpty', label: 'Is not empty' },
{ type: 'divider' },
{ key: 'yesterday', label: 'Yesterday', expression: '-1D' },
{ key: 'today', label: 'Today', expression: 'TODAY' },
{ key: 'tomorrow', label: 'Tomorrow', expression: '+1D' },
@ -298,7 +309,10 @@ const getFilterOptions = (category) => {
case 'object':
return [
{ key: 'equals', label: 'Equals', needsValue: true },
{ key: 'notEquals', label: 'Does not equal', needsValue: true }
{ key: 'notEquals', label: 'Does not equal', needsValue: true },
{ type: 'divider' },
{ key: 'isEmpty', label: 'Is empty' },
{ key: 'isNotEmpty', label: 'Is not empty' }
]
case 'text':
default:
@ -319,7 +333,8 @@ const getFilterOptions = (category) => {
{ key: 'equals', label: 'Equals', needsValue: true },
{ key: 'notEquals', label: 'Does not equal', needsValue: true },
{ type: 'divider' },
{ key: 'isEmpty', label: 'Is empty' }
{ key: 'isEmpty', label: 'Is empty' },
{ key: 'isNotEmpty', label: 'Is not empty' }
]
}
}
@ -396,8 +411,8 @@ const QuickPropertyFilters = ({
applyFilter(option.key)
return
}
if (option.key === 'isEmpty') {
applyFilter('isEmpty')
if (option.key === 'isEmpty' || option.key === 'isNotEmpty') {
applyFilter(option.key)
return
}
setInputValue(null)