Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/components/BrowserFilter/BrowserFilter.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,9 +491,21 @@ export default class BrowserFilter extends React.Component {
const date = new Date(compareTo.iso);
return filter.set('compareTo', date);
} else if (typeof compareTo === 'string' && !isNaN(Date.parse(compareTo))) {
// Convert date string to JavaScript Date
const date = new Date(compareTo);
return filter.set('compareTo', date);
// Only convert date strings to JavaScript Date if the field type is actually Date
const className = filter.get('class') || this.props.className;
const fieldName = filter.get('field');
const schema = this.props.schema;

if (schema && className && fieldName) {
const classSchema = schema[className];
const fieldType = classSchema?.[fieldName]?.type;

// Only convert to Date if the field type is actually Date
if (fieldType === 'Date') {
const date = new Date(compareTo);
return filter.set('compareTo', date);
}
}
}
// Leave JavaScript Date objects and other types unchanged
return filter;
Expand Down
194 changes: 184 additions & 10 deletions src/components/BrowserFilter/FilterRow.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import { Constraints } from 'lib/Filters';
import DateTimeEntry from 'components/DateTimeEntry/DateTimeEntry.react';
import Icon from 'components/Icon/Icon.react';
import Parse from 'parse';
import Popover from 'components/Popover/Popover.react';
import Position from 'lib/Position';
import PropTypes from 'lib/PropTypes';
import React, { useCallback } from 'react';
import React, { useCallback, useState, useRef } from 'react';
import styles from 'components/BrowserFilter/BrowserFilter.scss';
import validateNumeric from 'lib/validateNumeric';

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

const RegexOptionsButton = ({ modifiers, onChangeModifiers }) => {
const [showOptions, setShowOptions] = useState(false);
const buttonRef = useRef(null);
const dropdownRef = useRef(null);

// Parse modifiers string into individual flags
const modifiersArray = modifiers ? modifiers.split('') : [];
const hasI = modifiersArray.includes('i');
const hasU = modifiersArray.includes('u');
const hasM = modifiersArray.includes('m');
const hasX = modifiersArray.includes('x');
const hasS = modifiersArray.includes('s');

const toggleModifier = (modifier) => {
let newModifiers = [...modifiersArray];
if (newModifiers.includes(modifier)) {
newModifiers = newModifiers.filter(m => m !== modifier);
} else {
newModifiers.push(modifier);
}
onChangeModifiers(newModifiers.join(''));
};

React.useEffect(() => {
const handleClickOutside = (event) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target) &&
buttonRef.current &&
!buttonRef.current.contains(event.target)
) {
setShowOptions(false);
}
};

if (showOptions) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [showOptions]);

const optionsDropdown = showOptions ? (
<Popover
fixed={true}
position={Position.inDocument(buttonRef.current)}
data-popover-type="inner"
>
<div
ref={dropdownRef}
style={{
background: '#1e1e2e',
border: '1px solid #66637A',
borderRadius: '5px',
padding: '8px',
minWidth: '150px',
color: 'white',
fontSize: '14px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)'
}}
>
<div style={{ marginBottom: '4px', fontWeight: 'bold', paddingBottom: '4px', borderBottom: '1px solid rgba(255,255,255,0.2)' }}>
Regex Options
</div>
<label
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
onClick={() => toggleModifier('i')}
>
<input
type="checkbox"
checked={hasI}
readOnly
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
<span>Case insensitive (i)</span>
</label>
<label
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
onClick={() => toggleModifier('u')}
>
<input
type="checkbox"
checked={hasU}
readOnly
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
<span>Unicode (u)</span>
</label>
<label
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
onClick={() => toggleModifier('m')}
>
<input
type="checkbox"
checked={hasM}
readOnly
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
<span>Multiline (m)</span>
</label>
<label
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
onClick={() => toggleModifier('x')}
>
<input
type="checkbox"
checked={hasX}
readOnly
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
<span>Extended (x)</span>
</label>
<label
style={{ display: 'flex', alignItems: 'center', padding: '4px 0', cursor: 'pointer' }}
onClick={() => toggleModifier('s')}
>
<input
type="checkbox"
checked={hasS}
readOnly
style={{ marginRight: '8px', cursor: 'pointer' }}
/>
<span>Dotall (s)</span>
</label>
</div>
</Popover>
) : null;

return (
<>
<button
ref={buttonRef}
type="button"
className={styles.remove}
onClick={() => {
setShowOptions(!showOptions);
}}
title="Regex options"
>
<Icon name="gear-solid" width={14} height={14} fill="rgba(0,0,0,0.4)" />
</button>
{optionsDropdown}
</>
);
};

function compareValue(
info,
value,
Expand All @@ -29,7 +178,9 @@ function compareValue(
active,
parentContentId,
setFocus,
currentConstraint
currentConstraint,
modifiers,
onChangeModifiers
) {
if (currentConstraint === 'containedIn') {
return (
Expand Down Expand Up @@ -60,6 +211,21 @@ function compareValue(
return null;
case 'Object':
case 'String':
if (currentConstraint === 'matches') {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
<input
type="text"
value={value}
onChange={e => onChangeCompareTo(e.target.value)}
onKeyDown={onKeyDown}
ref={setFocus}
style={{ width: '106px' }}
/>
<RegexOptionsButton modifiers={modifiers} onChangeModifiers={onChangeModifiers} />
</div>
);
}
return (
<input
type="text"
Expand All @@ -85,6 +251,7 @@ function compareValue(
case 'Boolean':
return (
<ChromeDropdown
width="140"
color={active ? 'blue' : 'purple'}
value={value ? 'True' : 'False'}
options={['True', 'False']}
Expand Down Expand Up @@ -131,10 +298,12 @@ const FilterRow = ({
currentField,
currentConstraint,
compareTo,
modifiers,
onChangeClass,
onChangeField,
onChangeConstraint,
onChangeCompareTo,
onChangeModifiers,
onKeyDown,
onDeleteRow,
active,
Expand Down Expand Up @@ -234,13 +403,14 @@ const FilterRow = ({
buildSuggestions={buildFieldSuggestions}
buildLabel={() => ''}
/>
<ChromeDropdown
width={compareInfo.type ? '175' : '325'}
color={active ? 'blue' : 'purple'}
value={Constraints[currentConstraint].name}
options={constraints.map(c => Constraints[c].name)}
onChange={c => onChangeConstraint(constraintLookup[c], compareTo)}
/>
<div style={{ flex: 1 }}>
<ChromeDropdown
color={active ? 'blue' : 'purple'}
value={Constraints[currentConstraint].name}
options={constraints.map(c => Constraints[c].name)}
onChange={c => onChangeConstraint(constraintLookup[c], compareTo)}
/>
</div>
{compareValue(
compareInfo,
compareTo,
Expand All @@ -249,7 +419,9 @@ const FilterRow = ({
active,
parentContentId,
setFocus,
currentConstraint
currentConstraint,
modifiers,
onChangeModifiers
)}
<button type="button" className={styles.remove} onClick={onDeleteRow}>
<Icon name="minus-solid" width={14} height={14} fill="rgba(0,0,0,0.4)" />
Expand All @@ -267,4 +439,6 @@ FilterRow.propTypes = {
currentConstraint: PropTypes.string.isRequired,
compareTo: PropTypes.any,
compareInfo: PropTypes.object,
modifiers: PropTypes.string,
onChangeModifiers: PropTypes.func,
};
3 changes: 2 additions & 1 deletion src/components/ChromeDropdown/ChromeDropdown.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export default class ChromeDropdown extends React.Component {
}

render() {
let widthStyle = { width: parseFloat(this.props.width || 140) };
const width = this.props.width ? parseFloat(this.props.width) : '100%';
let widthStyle = { width };
const styles = this.styles;
const color = this.props.color || 'purple';

Expand Down
31 changes: 28 additions & 3 deletions src/components/Filter/Filter.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,27 @@ function changeConstraint(schema, currentClassName, filters, index, newConstrain
if (Object.prototype.hasOwnProperty.call(Filters.Constraints[newConstraint], 'field')) {
compareType = Filters.Constraints[newConstraint].field;
}

// Determine compareTo value
let compareTo;
if (newConstraint === 'containedIn') {
compareTo = [];
} else if (newConstraint === 'matches') {
// For matches constraint, always use empty string, don't reuse previous value
compareTo = '';
} else if (compareType && prevCompareTo && typeof prevCompareTo === typeof Filters.DefaultComparisons[compareType]) {
// Only reuse prevCompareTo if types match
compareTo = prevCompareTo;
} else {
compareTo = Filters.DefaultComparisons[compareType];
}

const newFilter = new Map({
class: currentClassName,
field: field,
constraint: newConstraint,
compareTo:
compareType && prevCompareTo ? prevCompareTo : newConstraint === 'containedIn' ? [] : Filters.DefaultComparisons[compareType],
compareTo,
modifiers: newConstraint === 'matches' ? 'i' : undefined,
});
return filters.set(index, newFilter);
}
Expand All @@ -72,6 +87,10 @@ function changeCompareTo(schema, filters, index, type, newCompare) {
return filters.set(index, filters.get(index).set('compareTo', newValue));
}

function changeModifiers(filters, index, newModifiers) {
return filters.set(index, filters.get(index).set('modifiers', newModifiers));
}

function deleteRow(filters, index) {
return filters.delete(index);
}
Expand All @@ -92,6 +111,7 @@ const Filter = ({
if (compare !== hasCompareTo) {
setCompare(hasCompareTo);
}

const currentApp = React.useContext(CurrentApp);
blacklist = blacklist || [];
const available = Filters.findRelatedClasses(className, allClasses, blacklist, filters);
Expand All @@ -114,7 +134,7 @@ const Filter = ({
>
<div style={{ width: '140px' }}>Class</div>
<div style={{ width: '140px' }}>Field</div>
<div style={{ width: '175px' }}>Condition</div>
<div style={compare ? { width: '175px' } : { flex: 1 }}>Condition</div>
{compare && <div>Value</div>}
<div></div>
</div>
Expand All @@ -124,6 +144,7 @@ const Filter = ({
const field = filter.get('field');
const constraint = filter.get('constraint');
const compareTo = filter.get('compareTo');
const modifiers = filter.get('modifiers');
let fields = [];
if (available[currentClassName]) {
fields = Object.keys(available[currentClassName]).concat([]);
Expand Down Expand Up @@ -182,6 +203,7 @@ const Filter = ({
currentField: field,
currentConstraint: constraint,
compareTo,
modifiers,
key: field + '-' + constraint + '-' + i,
onChangeClass: newClassName => {
onChange(changeClass(schema, filters, i, newClassName));
Expand All @@ -197,6 +219,9 @@ const Filter = ({
onChangeCompareTo: newCompare => {
onChange(changeCompareTo(schema, filters, i, compareType, newCompare));
},
onChangeModifiers: newModifiers => {
onChange(changeModifiers(filters, i, newModifiers));
},
onKeyDown: ({ key }) => {
if (key === 'Enter') {
onSearch();
Expand Down
Loading
Loading