Skip to content

Commit

Permalink
#9706: Allow time range filtering for Attribute Table quick filter (#…
Browse files Browse the repository at this point in the history
…9743)

* #9706: handle the advanced filter in attribute table with different operators

* #9706: Allow time range filtering for Attribute Table quick filter and allow additional filter operators for other attributes
Description:
- adding filter operators dropdown list in attribute table that includes range operator for time/date attributes
- creating a custom picker for range
- write unit tests
- Edit in style

* #9706: fix test failure by remove unused variable

* #9706: fix test failure by remove unused action in featuregrid file

* #9706: resolve review comments
Desciption:
- remove expression from filter inputs and make validation for the inputs
- Add cursor style to 'Start' & 'End' tabs
- Add validation for  ranges
- Replace 2 icons with one icon for date-time filter
- Fix style issues
- Fixed 'Start', 'End' tabs in scroll
- improve style of operator
- reset date/time if user change operator from range operator to another

* #9706: fix test case in AttributeFilter file

* #9706: Allow time range filtering for Attribute Table quick filter
* Description:
- Add date-time icon and use it
- replcae inline if
- using omit instead of reduce
- fix some style issues in date/time pickers in range component

* #9706: Allow time range filtering for Attribute Table quick filter
* Description:
- Fix FE test failure

* #9706: resolve reviewer comments [improve UI of pickers]
Description:
- Improve UI of pickers by using popover component in dateTime/Date/Hours pickers
- Enable update popover position in case of scroll the container conponent
-  write unit tests due to changes
- Rename range date-time picker by 'RangedDateTimePicker'

* #9706: resolve reviewer comments
Description:
- Handle highlight selected hours component
- Fix not alignment in dateTime picker
- Fix not alignment selected date in calendar for date/ dateTime pickers
- Minimize calendar row height to improve showing date picker UI
- Create a util function  'getLocalTimePart' to get time part for local time

* #9706: resolve FE test failure

* #9706: resolve reviewer comments
Description:
- Add placeholder for time picker
- Add translations for the added time placeholder
- Fix crash app bug due to entering invalid date in input texts of date/time Pickers

* #9706: hide input tooltip when user open pickers

* Time filter refactor

* unit test

---------

Co-authored-by: Suren <[email protected]>
  • Loading branch information
mahmoudadel54 and dsuren1 authored Jan 22, 2024
1 parent 35be6c1 commit 93ea6ea
Show file tree
Hide file tree
Showing 43 changed files with 2,527 additions and 862 deletions.
6 changes: 4 additions & 2 deletions web/client/components/data/featuregrid/enhancers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ const featuresToGrid = compose(
focusOnEdit: false,
editors,
dataStreamFactory,
virtualScroll: true
virtualScroll: true,
isWithinAttrTbl: false
}),
withPropsOnChange("showDragHandle", ({showDragHandle = true} = {}) => ({
className: showDragHandle ? 'feature-grid-drag-handle-show' : 'feature-grid-drag-handle-hide'
Expand Down Expand Up @@ -170,7 +171,8 @@ const featuresToGrid = compose(
return props.editors(desc.localType, generalProps);
},
getFilterRenderer: getFilterRendererFunc,
getFormatter: (desc) => getFormatter(desc, (props.fields ?? []).find(f => f.name === desc.name), {dateFormats: props.dateFormats})
getFormatter: (desc) => getFormatter(desc, (props.fields ?? []).find(f => f.name === desc.name), {dateFormats: props.dateFormats}),
isWithinAttrTbl: props.isWithinAttrTbl
}))
});
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import PropTypes from 'prop-types';
import { getMessageById } from '../../../../utils/LocaleUtils';
import { Tooltip } from 'react-bootstrap';
import OverlayTrigger from '../../../misc/OverlayTrigger';
import ComboField from '../../query/ComboField';

class AttributeFilter extends React.PureComponent {
static propTypes = {
Expand All @@ -21,7 +22,10 @@ class AttributeFilter extends React.PureComponent {
value: PropTypes.any,
column: PropTypes.object,
placeholderMsgId: PropTypes.string,
tooltipMsgId: PropTypes.string
tooltipMsgId: PropTypes.string,
operator: PropTypes.string,
type: PropTypes.string,
isWithinAttrTbl: PropTypes.bool
};

static contextTypes = {
Expand All @@ -33,15 +37,66 @@ class AttributeFilter extends React.PureComponent {
valid: true,
onChange: () => {},
column: {},
placeholderMsgId: "featuregrid.filter.placeholders.default"
placeholderMsgId: "featuregrid.filter.placeholders.default",
operator: "=",
isWithinAttrTbl: false
};
constructor(props) {
super(props);
this.state = {
listOperators: ["="],
stringOperators: ["=", "<>", "like", "ilike", "isNull"],
arrayOperators: ["contains"],
booleanOperators: ["="],
defaultOperators: ["=", ">", "<", ">=", "<=", "<>", "isNull"],
timeDateOperators: ["=", ">", "<", ">=", "<=", "<>", "><", "isNull"],
operator: this.props.isWithinAttrTbl ? "=" : "",
isInputValid: true
};
}
getOperator = (type) => {
switch (type) {
case "list": {
return this.state.listOperators;
}
case "string": {
return this.state.stringOperators;
}
case "boolean": {
return this.state.booleanOperators;
}
case "array": {
return this.state.arrayOperators;
}
case "date":
case "time":
case "date-time":
{
return this.state.timeDateOperators;
}
default:
return this.state.defaultOperators;
}
};
renderInput = () => {
if (this.props.column.filterable === false) {
return <span/>;
}
const placeholder = getMessageById(this.context.messages, this.props.placeholderMsgId) || "Search";
let inputKey = 'header-filter-' + this.props.column.key;
return (<input disabled={this.props.disabled} key={inputKey} type="text" className="form-control input-sm" placeholder={placeholder} value={this.state?.value ?? this.props.value} onChange={this.handleChange}/>);
let isValueExist = this.state?.value ?? this.props.value;
if (['date', 'time', 'date-time'].includes(this.props.type) && this.props.isWithinAttrTbl) isValueExist = this.state?.value ?? this.props.value?.startDate ?? this.props.value;
let isNullOperator = this.state.operator === 'isNull';
return (<div className={`rw-widget ${this.state.isInputValid ? "" : "show-error"}`}>
<input
disabled={this.props.disabled || isNullOperator}
key={inputKey}
type="text"
className={"form-control input-sm"}
placeholder={placeholder}
value={isValueExist}
onChange={this.handleChange}/>
</div>);
}
renderTooltip = (cmp) => {
if (this.props.tooltipMsgId && getMessageById(this.context.messages, this.props.tooltipMsgId)) {
Expand All @@ -51,19 +106,67 @@ class AttributeFilter extends React.PureComponent {
}
return cmp;
}

renderOperatorField = () => {
return (
<ComboField
style={{ width: '90px'}}
fieldOptions= {this.getOperator(this.props.type)}
fieldName="operator"
fieldRowId={1}
onSelect={(selectedOperator)=>{
// if select the same operator -> don't do anything
if (selectedOperator === this.state.operator) return;
let isValueExist; // entered value
if (['date', 'time', 'date-time'].includes(this.props.type)) {
isValueExist = this.state?.value ?? this.props.value?.startDate ?? this.props.value;
} else {
isValueExist = this.state?.value ?? this.props.value;
}
let isNullOperatorSelected = selectedOperator === 'isNull';
let isOperatorChangedFromRange = this.state.operator === '><';
// set the selected operator + value and reset the value in case of isNull
this.setState({ operator: selectedOperator, value: (isNullOperatorSelected || isOperatorChangedFromRange) ? undefined : isValueExist });
// get flag of being (operator was isNull then changes to other operator)
let isOperatorChangedFromIsNull = this.state.operator === 'isNull' && selectedOperator !== 'isNull';
// apply filter if value exists 'OR' operator = isNull 'OR' (prev operator was isNull and changes --> reset filter)
if (isNullOperatorSelected || isOperatorChangedFromIsNull || isOperatorChangedFromRange) {
// reset data --> operator = isNull 'OR' (prev operator was isNull and changes)
this.props.onChange({value: null, attribute: this.props.column && this.props.column.key, inputOperator: selectedOperator});
} else if (isValueExist) {
// apply filter --> if value exists
this.props.onChange({value: isValueExist, attribute: this.props.column && this.props.column.key, inputOperator: selectedOperator});
}
}}
fieldValue={this.state.operator}
onUpdateField={() => {}}/>
);
};
render() {
let inputKey = 'header-filter--' + this.props.column.key;
return (
<div key={inputKey} className={`form-group${(this.props.valid ? "" : " has-error")}`}>
{this.renderTooltip(this.renderInput())}
<div key={inputKey} className={`form-group${((this.state.isInputValid && this.props.valid) ? "" : " has-error")}`}>
{this.props.isWithinAttrTbl ? <>
{this.renderOperatorField()}
{['time', 'date', 'date-time'].includes(this.props.type) ? this.renderInput() : this.renderTooltip(this.renderInput())}
</> : this.renderTooltip(this.renderInput())}
</div>
);
}
handleChange = (e) => {
const value = e.target.value;
this.setState({value});
this.props.onChange({value, attribute: this.props.column && this.props.column.key});
// todo: validate input based on type
let isValid = true;
if (this.props.isWithinAttrTbl) {
const match = /\s*(!==|!=|<>|<=|>=|===|==|=|<|>)?(.*)/.exec(value);
if (match[1]) isValid = false;
if (match[2]) {
if (['integer', 'number'].includes(this.props.type) && isNaN(match[2])) isValid = false;
}
}
this.setState({value, isInputValid: isValid});
if (isValid) {
this.props.onChange({value, attribute: this.props.column && this.props.column.key, inputOperator: this.state.operator});
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {getContext} from 'recompose';
import DateTimePicker from '../../../misc/datetimepicker';
import RangedDateTimePicker from '../../../misc/datetimepicker/RangedDateTimePicker';
import {getMessageById} from '../../../../utils/LocaleUtils';
import { getDateTimeFormat } from '../../../../utils/TimeUtils';
import AttributeFilter from './AttributeFilter';
Expand All @@ -22,6 +23,12 @@ const UTCDateTimePicker = utcDateWrapper({
setDateProp: "onChange"
})(DateTimePicker);

const UTCDateTimePickerWithRange = utcDateWrapper({
dateProp: "value",
dateTypeProp: "type",
setDateProp: "onChange"
})(RangedDateTimePicker );


class DateFilter extends AttributeFilter {
static propTypes = {
Expand All @@ -45,6 +52,7 @@ class DateFilter extends AttributeFilter {
if (this.props.column.filterable === false) {
return <span />;
}
const operator = this.props.value && this.props.value.operator || this.state.operator;
const format = getDateTimeFormat(this.context.locale, this.props.type);
const placeholder = getMessageById(this.context.messages, this.props.placeholderMsgId) || "Insert date";
const toolTip = this.props.intl && this.props.intl.formatMessage({id: `${this.props.tooltipMsgId}`}, {format}) || `Insert date in ${format} format`;
Expand All @@ -58,8 +66,27 @@ class DateFilter extends AttributeFilter {
val = this.props.value && this.props.value.startDate || this.props.value;
}
const dateValue = this.props.value ? val : null;
const operator = this.props.value && this.props.value.operator;
if (operator === '><') {
return (
<UTCDateTimePickerWithRange
isWithinAttrTbl={this.props.isWithinAttrTbl}
key={inputKey}
disabled={this.props.disabled}
format={format}
placeholder={placeholder}
value={dateValue}
toolTip={toolTip}
popupPosition={'top'} // popover open direction
operator={operator}
type={this.props.type}
time={this.props.type === 'time'}
calendar={this.props.type === 'date-time' || this.props.type === 'date'}
onChange={(date, stringDate, order) => this.handleChangeRangeFilter(date, stringDate, order)}
/>
);
}
return (<UTCDateTimePicker
isWithinAttrTbl={this.props.isWithinAttrTbl}
key={inputKey}
disabled={this.props.disabled}
format={format}
Expand All @@ -68,13 +95,29 @@ class DateFilter extends AttributeFilter {
toolTip={toolTip}
operator={operator}
type={this.props.type}
popupPosition={'top'} // popover open direction
time={this.props.type === 'date-time' || this.props.type === 'time'}
calendar={this.props.type === 'date-time' || this.props.type === 'date'}
onChange={(date, stringDate) => this.handleChange(date, stringDate)}
/>);
}
handleChange = (value, stringValue) => {
this.props.onChange({ value, stringValue, attribute: this.props.column && this.props.column.name });
this.props.onChange({ value, stringValue, attribute: this.props.column && this.props.column.name, inputOperator: this.state.operator || this.props.operator });
}
handleChangeRangeFilter = (value, stringValue, order = 'start') => {
let reqVal = {};
if (order === 'end') {
reqVal = {
startDate: this.props.value?.startDate,
endDate: value
};
} else {
reqVal = {
startDate: value,
endDate: this.props.value?.endDate
};
}
this.props.onChange({ value: reqVal, stringValue, attribute: this.props.column && this.props.column.name, inputOperator: this.state.operator || this.props.operator });
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,41 @@ export default compose(
value: null
}),
withHandlers({
onChange: props => ({ value, attribute, stringValue } = {}) => {
const match = /\s*(!==|!=|<>|<=|>=|===|==|=|<|>)?(.*)/.exec(stringValue);
const operator = match[1];
let enhancedOperator = match[1] || '=';
// replace with standard operators
if (operator === "!==" | operator === "!=") {
enhancedOperator = "<>";
} else if (operator === "===" | operator === "==") {
enhancedOperator = "=";
onChange: props => ({ value, attribute, stringValue, inputOperator } = {}) => {
if (typeof value === 'string') {
const match = /\s*(!==|!=|<>|<=|>=|===|==|=|<|>)?(.*)/.exec(stringValue);
const operator = match[1];
let enhancedOperator = match[1] || '=';
// replace with standard operators
if (operator === "!==" | operator === "!=") {
enhancedOperator = "<>";
} else if (operator === "===" | operator === "==") {
enhancedOperator = "=";
}
props.onValueChange(value);
props.onChange({
value: { startDate: value, operator: inputOperator || operator },
operator: inputOperator || enhancedOperator,
type: props.type,
attribute
});
} else if (value && typeof value === 'object') {
props.onValueChange(value);
props.onChange({
value: { startDate: value?.startDate, endDate: value?.endDate, operator: inputOperator },
operator: inputOperator,
type: props.type,
attribute
});
} else if (!value) {
props.onValueChange(value);
props.onChange({
value: { startDate: value, operator: inputOperator },
operator: inputOperator,
type: props.type,
attribute
});
}
props.onValueChange(value);
props.onChange({
value: { startDate: value, operator },
operator: enhancedOperator,
type: props.type,
attribute
});
}
}),
defaultProps({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ export default compose(
onValueChange: () => {}
}),
withHandlers({
onChange: props => ({value, attribute} = {}) => {
onChange: props => ({value, attribute, inputOperator} = {}) => {
props.onValueChange(value);
props.onChange({
value: value,
operator: "=",
operator: inputOperator || "=",
type: props.type,
attribute
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default compose(
}),
withState("valid", "setValid", true),
withHandlers({
onChange: props => ({value, attribute} = {}) => {
onChange: props => ({value, attribute, inputOperator} = {}) => {
props.onValueChange(value);
if (!COMMA_REGEX.exec(value)) {
let {operator, newVal} = getOperatorAndValue(value, "number");
Expand All @@ -31,7 +31,7 @@ export default compose(
props.onChange({
value: isNaN(newVal) ? undefined : newVal,
rawValue: value,
operator,
operator: inputOperator || operator,
type: 'number',
attribute
});
Expand All @@ -48,7 +48,7 @@ export default compose(
isValid && props.onChange({
value,
rawValue: value,
operator: "=",
operator: inputOperator || "=",
type: 'number',
attribute
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ export default compose(
placeholderMsgId: "featuregrid.filter.placeholders.string"
}),
withHandlers({
onChange: props => ({value, attribute} = {}) => {
onChange: props => ({value, attribute, inputOperator} = {}) => {
props.onValueChange(value);
props.onChange({
rawValue: value,
value: trim(value) ? trim(value) : undefined,
operator: "ilike",
operator: inputOperator || "ilike",
type: 'string',
attribute
});
Expand Down
Loading

0 comments on commit 93ea6ea

Please sign in to comment.