Skip to content

Commit 64a9f71

Browse files
authored
feat: Add matches regex filter to data browser replacing limited string contains string filter (#2991)
1 parent 0da00c7 commit 64a9f71

File tree

8 files changed

+335
-73
lines changed

8 files changed

+335
-73
lines changed

src/components/BrowserFilter/BrowserFilter.react.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,21 @@ export default class BrowserFilter extends React.Component {
491491
const date = new Date(compareTo.iso);
492492
return filter.set('compareTo', date);
493493
} else if (typeof compareTo === 'string' && !isNaN(Date.parse(compareTo))) {
494-
// Convert date string to JavaScript Date
495-
const date = new Date(compareTo);
496-
return filter.set('compareTo', date);
494+
// Only convert date strings to JavaScript Date if the field type is actually Date
495+
const className = filter.get('class') || this.props.className;
496+
const fieldName = filter.get('field');
497+
const schema = this.props.schema;
498+
499+
if (schema && className && fieldName) {
500+
const classSchema = schema[className];
501+
const fieldType = classSchema?.[fieldName]?.type;
502+
503+
// Only convert to Date if the field type is actually Date
504+
if (fieldType === 'Date') {
505+
const date = new Date(compareTo);
506+
return filter.set('compareTo', date);
507+
}
508+
}
497509
}
498510
// Leave JavaScript Date objects and other types unchanged
499511
return filter;

src/components/BrowserFilter/FilterRow.react.js

Lines changed: 184 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { Constraints } from 'lib/Filters';
1111
import DateTimeEntry from 'components/DateTimeEntry/DateTimeEntry.react';
1212
import Icon from 'components/Icon/Icon.react';
1313
import Parse from 'parse';
14+
import Popover from 'components/Popover/Popover.react';
15+
import Position from 'lib/Position';
1416
import PropTypes from 'lib/PropTypes';
15-
import React, { useCallback } from 'react';
17+
import React, { useCallback, useState, useRef } from 'react';
1618
import styles from 'components/BrowserFilter/BrowserFilter.scss';
1719
import validateNumeric from 'lib/validateNumeric';
1820

@@ -21,6 +23,153 @@ for (const c in Constraints) {
2123
constraintLookup[Constraints[c].name] = c;
2224
}
2325

26+
const RegexOptionsButton = ({ modifiers, onChangeModifiers }) => {
27+
const [showOptions, setShowOptions] = useState(false);
28+
const buttonRef = useRef(null);
29+
const dropdownRef = useRef(null);
30+
31+
// Parse modifiers string into individual flags
32+
const modifiersArray = modifiers ? modifiers.split('') : [];
33+
const hasI = modifiersArray.includes('i');
34+
const hasU = modifiersArray.includes('u');
35+
const hasM = modifiersArray.includes('m');
36+
const hasX = modifiersArray.includes('x');
37+
const hasS = modifiersArray.includes('s');
38+
39+
const toggleModifier = (modifier) => {
40+
let newModifiers = [...modifiersArray];
41+
if (newModifiers.includes(modifier)) {
42+
newModifiers = newModifiers.filter(m => m !== modifier);
43+
} else {
44+
newModifiers.push(modifier);
45+
}
46+
onChangeModifiers(newModifiers.join(''));
47+
};
48+
49+
React.useEffect(() => {
50+
const handleClickOutside = (event) => {
51+
if (
52+
dropdownRef.current &&
53+
!dropdownRef.current.contains(event.target) &&
54+
buttonRef.current &&
55+
!buttonRef.current.contains(event.target)
56+
) {
57+
setShowOptions(false);
58+
}
59+
};
60+
61+
if (showOptions) {
62+
document.addEventListener('mousedown', handleClickOutside);
63+
return () => {
64+
document.removeEventListener('mousedown', handleClickOutside);
65+
};
66+
}
67+
}, [showOptions]);
68+
69+
const optionsDropdown = showOptions ? (
70+
<Popover
71+
fixed={true}
72+
position={Position.inDocument(buttonRef.current)}
73+
data-popover-type="inner"
74+
>
75+
<div
76+
ref={dropdownRef}
77+
style={{
78+
background: '#1e1e2e',
79+
border: '1px solid #66637A',
80+
borderRadius: '5px',
81+
padding: '8px',
82+
minWidth: '150px',
83+
color: 'white',
84+
fontSize: '14px',
85+
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'
86+
}}
87+
>
88+
<div style={{ marginBottom: '4px', fontWeight: 'bold', paddingBottom: '4px', borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
89+
Regex Options
90+
</div>
91+
<label
92+
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
93+
onClick={() => toggleModifier('i')}
94+
>
95+
<input
96+
type="checkbox"
97+
checked={hasI}
98+
readOnly
99+
style={{ marginRight: '8px', cursor: 'pointer' }}
100+
/>
101+
<span>Case insensitive (i)</span>
102+
</label>
103+
<label
104+
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
105+
onClick={() => toggleModifier('u')}
106+
>
107+
<input
108+
type="checkbox"
109+
checked={hasU}
110+
readOnly
111+
style={{ marginRight: '8px', cursor: 'pointer' }}
112+
/>
113+
<span>Unicode (u)</span>
114+
</label>
115+
<label
116+
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
117+
onClick={() => toggleModifier('m')}
118+
>
119+
<input
120+
type="checkbox"
121+
checked={hasM}
122+
readOnly
123+
style={{ marginRight: '8px', cursor: 'pointer' }}
124+
/>
125+
<span>Multiline (m)</span>
126+
</label>
127+
<label
128+
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
129+
onClick={() => toggleModifier('x')}
130+
>
131+
<input
132+
type="checkbox"
133+
checked={hasX}
134+
readOnly
135+
style={{ marginRight: '8px', cursor: 'pointer' }}
136+
/>
137+
<span>Extended (x)</span>
138+
</label>
139+
<label
140+
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
141+
onClick={() => toggleModifier('s')}
142+
>
143+
<input
144+
type="checkbox"
145+
checked={hasS}
146+
readOnly
147+
style={{ marginRight: '8px', cursor: 'pointer' }}
148+
/>
149+
<span>Dotall (s)</span>
150+
</label>
151+
</div>
152+
</Popover>
153+
) : null;
154+
155+
return (
156+
<>
157+
<button
158+
ref={buttonRef}
159+
type="button"
160+
className={styles.remove}
161+
onClick={() => {
162+
setShowOptions(!showOptions);
163+
}}
164+
title="Regex options"
165+
>
166+
<Icon name="gear-solid" width={14} height={14} fill="rgba(0,0,0,0.4)" />
167+
</button>
168+
{optionsDropdown}
169+
</>
170+
);
171+
};
172+
24173
function compareValue(
25174
info,
26175
value,
@@ -29,7 +178,9 @@ function compareValue(
29178
active,
30179
parentContentId,
31180
setFocus,
32-
currentConstraint
181+
currentConstraint,
182+
modifiers,
183+
onChangeModifiers
33184
) {
34185
if (currentConstraint === 'containedIn') {
35186
return (
@@ -60,6 +211,21 @@ function compareValue(
60211
return null;
61212
case 'Object':
62213
case 'String':
214+
if (currentConstraint === 'matches') {
215+
return (
216+
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
217+
<input
218+
type="text"
219+
value={value}
220+
onChange={e => onChangeCompareTo(e.target.value)}
221+
onKeyDown={onKeyDown}
222+
ref={setFocus}
223+
style={{ width: '106px' }}
224+
/>
225+
<RegexOptionsButton modifiers={modifiers} onChangeModifiers={onChangeModifiers} />
226+
</div>
227+
);
228+
}
63229
return (
64230
<input
65231
type="text"
@@ -85,6 +251,7 @@ function compareValue(
85251
case 'Boolean':
86252
return (
87253
<ChromeDropdown
254+
width="140"
88255
color={active ? 'blue' : 'purple'}
89256
value={value ? 'True' : 'False'}
90257
options={['True', 'False']}
@@ -131,10 +298,12 @@ const FilterRow = ({
131298
currentField,
132299
currentConstraint,
133300
compareTo,
301+
modifiers,
134302
onChangeClass,
135303
onChangeField,
136304
onChangeConstraint,
137305
onChangeCompareTo,
306+
onChangeModifiers,
138307
onKeyDown,
139308
onDeleteRow,
140309
active,
@@ -234,13 +403,14 @@ const FilterRow = ({
234403
buildSuggestions={buildFieldSuggestions}
235404
buildLabel={() => ''}
236405
/>
237-
<ChromeDropdown
238-
width={compareInfo.type ? '175' : '325'}
239-
color={active ? 'blue' : 'purple'}
240-
value={Constraints[currentConstraint].name}
241-
options={constraints.map(c => Constraints[c].name)}
242-
onChange={c => onChangeConstraint(constraintLookup[c], compareTo)}
243-
/>
406+
<div style={{ flex: 1 }}>
407+
<ChromeDropdown
408+
color={active ? 'blue' : 'purple'}
409+
value={Constraints[currentConstraint].name}
410+
options={constraints.map(c => Constraints[c].name)}
411+
onChange={c => onChangeConstraint(constraintLookup[c], compareTo)}
412+
/>
413+
</div>
244414
{compareValue(
245415
compareInfo,
246416
compareTo,
@@ -249,7 +419,9 @@ const FilterRow = ({
249419
active,
250420
parentContentId,
251421
setFocus,
252-
currentConstraint
422+
currentConstraint,
423+
modifiers,
424+
onChangeModifiers
253425
)}
254426
<button type="button" className={styles.remove} onClick={onDeleteRow}>
255427
<Icon name="minus-solid" width={14} height={14} fill="rgba(0,0,0,0.4)" />
@@ -267,4 +439,6 @@ FilterRow.propTypes = {
267439
currentConstraint: PropTypes.string.isRequired,
268440
compareTo: PropTypes.any,
269441
compareInfo: PropTypes.object,
442+
modifiers: PropTypes.string,
443+
onChangeModifiers: PropTypes.func,
270444
};

src/components/ChromeDropdown/ChromeDropdown.react.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ export default class ChromeDropdown extends React.Component {
5353
}
5454

5555
render() {
56-
let widthStyle = { width: parseFloat(this.props.width || 140) };
56+
const width = this.props.width ? parseFloat(this.props.width) : '100%';
57+
let widthStyle = { width };
5758
const styles = this.styles;
5859
const color = this.props.color || 'purple';
5960

src/components/Filter/Filter.react.js

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,27 @@ function changeConstraint(schema, currentClassName, filters, index, newConstrain
5757
if (Object.prototype.hasOwnProperty.call(Filters.Constraints[newConstraint], 'field')) {
5858
compareType = Filters.Constraints[newConstraint].field;
5959
}
60+
61+
// Determine compareTo value
62+
let compareTo;
63+
if (newConstraint === 'containedIn') {
64+
compareTo = [];
65+
} else if (newConstraint === 'matches') {
66+
// For matches constraint, always use empty string, don't reuse previous value
67+
compareTo = '';
68+
} else if (compareType && prevCompareTo && typeof prevCompareTo === typeof Filters.DefaultComparisons[compareType]) {
69+
// Only reuse prevCompareTo if types match
70+
compareTo = prevCompareTo;
71+
} else {
72+
compareTo = Filters.DefaultComparisons[compareType];
73+
}
74+
6075
const newFilter = new Map({
6176
class: currentClassName,
6277
field: field,
6378
constraint: newConstraint,
64-
compareTo:
65-
compareType && prevCompareTo ? prevCompareTo : newConstraint === 'containedIn' ? [] : Filters.DefaultComparisons[compareType],
79+
compareTo,
80+
modifiers: newConstraint === 'matches' ? 'i' : undefined,
6681
});
6782
return filters.set(index, newFilter);
6883
}
@@ -72,6 +87,10 @@ function changeCompareTo(schema, filters, index, type, newCompare) {
7287
return filters.set(index, filters.get(index).set('compareTo', newValue));
7388
}
7489

90+
function changeModifiers(filters, index, newModifiers) {
91+
return filters.set(index, filters.get(index).set('modifiers', newModifiers));
92+
}
93+
7594
function deleteRow(filters, index) {
7695
return filters.delete(index);
7796
}
@@ -92,6 +111,7 @@ const Filter = ({
92111
if (compare !== hasCompareTo) {
93112
setCompare(hasCompareTo);
94113
}
114+
95115
const currentApp = React.useContext(CurrentApp);
96116
blacklist = blacklist || [];
97117
const available = Filters.findRelatedClasses(className, allClasses, blacklist, filters);
@@ -114,7 +134,7 @@ const Filter = ({
114134
>
115135
<div style={{ width: '140px' }}>Class</div>
116136
<div style={{ width: '140px' }}>Field</div>
117-
<div style={{ width: '175px' }}>Condition</div>
137+
<div style={compare ? { width: '175px' } : { flex: 1 }}>Condition</div>
118138
{compare && <div>Value</div>}
119139
<div></div>
120140
</div>
@@ -124,6 +144,7 @@ const Filter = ({
124144
const field = filter.get('field');
125145
const constraint = filter.get('constraint');
126146
const compareTo = filter.get('compareTo');
147+
const modifiers = filter.get('modifiers');
127148
let fields = [];
128149
if (available[currentClassName]) {
129150
fields = Object.keys(available[currentClassName]).concat([]);
@@ -182,6 +203,7 @@ const Filter = ({
182203
currentField: field,
183204
currentConstraint: constraint,
184205
compareTo,
206+
modifiers,
185207
key: field + '-' + constraint + '-' + i,
186208
onChangeClass: newClassName => {
187209
onChange(changeClass(schema, filters, i, newClassName));
@@ -197,6 +219,9 @@ const Filter = ({
197219
onChangeCompareTo: newCompare => {
198220
onChange(changeCompareTo(schema, filters, i, compareType, newCompare));
199221
},
222+
onChangeModifiers: newModifiers => {
223+
onChange(changeModifiers(filters, i, newModifiers));
224+
},
200225
onKeyDown: ({ key }) => {
201226
if (key === 'Enter') {
202227
onSearch();

0 commit comments

Comments
 (0)