diff --git a/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx index 475dcced87..f08abe4785 100644 --- a/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx @@ -50,7 +50,8 @@ class AttributeFilter extends React.PureComponent { booleanOperators: ["="], defaultOperators: ["=", ">", "<", ">=", "<=", "<>", "isNull"], timeDateOperators: ["=", ">", "<", ">=", "<=", "<>", "><", "isNull"], - operator: this.props.isWithinAttrTbl ? "=" : "" + operator: this.props.isWithinAttrTbl ? "=" : "", + isInputValid: true }; } getOperator = (type) => { @@ -86,12 +87,12 @@ class AttributeFilter extends React.PureComponent { let isValueExist = this.state?.value ?? this.props.value; if (['date', 'time', 'date-time'].includes(this.props.type)) isValueExist = this.state?.value ?? this.props.value?.startDate ?? this.props.value; let isNullOperator = this.state.operator === 'isNull'; - return (
+ return (
@@ -108,19 +109,33 @@ class AttributeFilter extends React.PureComponent { renderOperatorField = () => { return ( { + // if select the same operator -> don't do anything if (selectedOperator === this.state.operator) return; - let isValueExist = this.state?.value ?? this.props.value; - if (['date', 'time', 'date-time'].includes(this.props.type)) isValueExist = this.state?.value ?? this.props.value?.startDate ?? this.props.value; - this.setState({ operator: selectedOperator, value: selectedOperator === 'isNull' ? undefined : isValueExist }); + 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 isOperatorChangedFromIsNullAndValueNotExist = this.state.operator === 'isNull' && this.state.operator !== selectedOperator && !isValueExist; - if (isValueExist || isNullOperatorSelected || isOperatorChangedFromIsNullAndValueNotExist ) this.props.onChange({value: isNullOperatorSelected ? null : isValueExist, attribute: this.props.column && this.props.column.key, inputOperator: selectedOperator}); + 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={() => {}}/> @@ -129,7 +144,7 @@ class AttributeFilter extends React.PureComponent { render() { let inputKey = 'header-filter--' + this.props.column.key; return ( -
+
{this.props.isWithinAttrTbl ? this.renderOperatorField() : null} {this.renderTooltip(this.renderInput())}
@@ -137,8 +152,15 @@ class AttributeFilter extends React.PureComponent { } handleChange = (e) => { const value = e.target.value; - this.setState({value}); - this.props.onChange({value, attribute: this.props.column && this.props.column.key, inputOperator: this.state.operator}); + // todo: validate input based on type + let isValid = true; + 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}); } } diff --git a/web/client/components/data/featuregrid/filterRenderers/__tests__/NumberFilter-test.jsx b/web/client/components/data/featuregrid/filterRenderers/__tests__/NumberFilter-test.jsx index 04af11804b..722c68b837 100644 --- a/web/client/components/data/featuregrid/filterRenderers/__tests__/NumberFilter-test.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/__tests__/NumberFilter-test.jsx @@ -33,15 +33,17 @@ const EXPRESSION_TESTS = [ [" ", "=", undefined], ["ZZZ", "=", undefined] ]; -const testExpression = (spyonChange, spyonValueChange, rawValue, expectedOperator, expectedValue) => { +const testExpression = (spyonChange, spyonValueChange, rawValue, expectedOperator, expectedValue, index) => { const input = document.getElementsByTagName("input")[0]; input.value = rawValue; ReactTestUtils.Simulate.change(input); - const args = spyonChange.calls[spyonChange.calls.length - 1].arguments[0]; - const valueArgs = spyonValueChange.calls[spyonValueChange.calls.length - 1].arguments[0]; - expect(args.value).toBe(expectedValue); - expect(args.operator).toBe(expectedOperator); - expect(valueArgs).toBe(rawValue); + const args = spyonChange.calls[index]?.arguments[0]; + const valueArgs = spyonValueChange.calls[index]?.arguments[0]; + if (valueArgs) { // in case of invalid number expression it will be undefined + expect(args.value).toBe(expectedValue); + expect(args.operator).toBe(expectedOperator); + expect(valueArgs).toBe(rawValue); + } }; describe('Test for NumberFilter component', () => { @@ -75,7 +77,7 @@ describe('Test for NumberFilter component', () => { ReactDOM.render(, document.getElementById("container")); const input = document.getElementsByTagName("input")[0]; - input.value = "> 2"; + input.value = "2"; ReactTestUtils.Simulate.change(input); expect(spyonChange).toHaveBeenCalled(); }); @@ -115,6 +117,6 @@ describe('Test for NumberFilter component', () => { const spyonChange = expect.spyOn(actions, 'onChange'); const spyonValueChange = expect.spyOn(actions, 'onValueChange'); ReactDOM.render(, document.getElementById("container")); - EXPRESSION_TESTS.map( params => testExpression(spyonChange, spyonValueChange, ...params)); + EXPRESSION_TESTS.map( (params, index) => testExpression(spyonChange, spyonValueChange, ...params, index)); }); }); diff --git a/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js b/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js index 2f9f0e426b..0aa42a4812 100644 --- a/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js +++ b/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js @@ -42,7 +42,7 @@ const formats = { }; /** - * @name DateTimePicker + * @name DateTimePickerWithRange * The revised react-widget datetimepicker to support operator in addition to date and time. * This component mimick the react-widget date time picker component behaviours and * props. Please see https://jquense.github.io/react-widgets/api/DateTimePicker/. @@ -51,7 +51,7 @@ const formats = { * considered as operator by this component. * */ -class DateTimePicker extends Component { +class DateTimePickerWithRange extends Component { static propTypes = { format: PropTypes.string, @@ -89,11 +89,12 @@ class DateTimePicker extends Component { startDate: '', endDate: '' }, - operator: '', + operator: '><', date: { // stored values startDate: null, endDate: null - } + }, + isInputValid: false } componentDidMount() { @@ -114,20 +115,20 @@ class DateTimePicker extends Component { return format ? format : !time && calendar ? dateFormat : time && !calendar ? timeFormat : defaultFormat; } - renderInput = (inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible, disabled, isFullWidth) => { + renderInput = (inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible, style) => { let inputV = this.props.isWithinAttrTbl ? `${inputValue}` : `${operator}${inputValue}`; if (toolTip) { return ({toolTip}}> - + ); } - return (); + return (); } renderHoursRange = () =>{ const { inputValue, operator, focused, openRangeInputs, openTime } = this.state; const { placeholder, tabIndex } = this.props; const props = Object.keys(this.props).reduce((acc, key) => { - if (['placeholder', 'calendar', 'time', 'onChange', 'value'].includes(key)) { + if (['placeholder', 'calendar', 'time', 'onChange', 'value', 'toolTip', 'onMouseOver'].includes(key)) { // remove these props because they might have undesired effects to the subsequent components return acc; } @@ -136,23 +137,23 @@ class DateTimePicker extends Component { }, {}); return ( -
+
-
+
Start {inputValue.startDate || 'Please Enter ...' }
-
+
End {inputValue.endDate || 'Please Enter ...' }
-
-
+
+
{this.renderInput(inputValue.startDate, operator, '', placeholder, tabIndex, false, true)}
-
+
{this.renderInput(inputValue.endDate, operator, '', placeholder, tabIndex, false, true)} + +
+
+ +
+
+
+
+
+ ); + }; + renderInput = (inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible, style = {}) => { let inputV = this.props.isWithinAttrTbl ? `${inputValue}` : `${operator}${inputValue}`; let isNullOperator = this.props.operator === 'isNull'; if (isNullOperator) inputV = ''; if (toolTip) { return ({toolTip}}> - + ); } - return (); + return (); } render() { - const { open, inputValue, operator, focused } = this.state; - const { calendar, time, toolTip, placeholder, tabIndex, popupPosition } = this.props; + const { open, inputValue, operator, focused, openDateTime } = this.state; + const { calendar, time, toolTip, placeholder, tabIndex, popupPosition, type } = this.props; const props = Object.keys(this.props).reduce((acc, key) => { if (['placeholder', 'calendar', 'time', 'onChange', 'value'].includes(key)) { // remove these props because they might have undesired effects to the subsequent components @@ -133,12 +180,28 @@ class DateTimePicker extends Component { }, {}); const calendarVisible = open === 'date'; const timeVisible = open === 'time'; + const dateTimeVisible = openDateTime === 'dateTime'; let calendarVal; if ( this.props.value && typeof this.props.value === 'object') { calendarVal = this.props.value?.startDate; } else if (this.props.value && typeof this.props.value === 'string') calendarVal = this.props.value; + if (type === 'date-time') { + return (
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, true, true)} + + + + { dateTimeVisible && <> +
+ {this.renderCustomDateTimePopup()} +
+ } +
); + } return ( -
+
{this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible)} {calendar || time ? @@ -155,7 +218,7 @@ class DateTimePicker extends Component { : '' } -
+
@@ -185,11 +248,12 @@ class DateTimePicker extends Component { this.ignoreBlur = false; } - handleWidgetBlur = () => { + handleWidgetBlur = (type) => { if (this.ignoreBlur) { return; } - this.setState({ open: '', focused: false }); + if (type === 'dateTime') this.setState({ openDateTime: '', focused: false }); + else this.setState({ open: '', focused: false }); } handleMouseDown = () => { @@ -199,7 +263,9 @@ class DateTimePicker extends Component { toggleCalendar = () => { this.setState(prevState => ({ open: prevState.open !== 'date' ? 'date' : '' })); } - + toggleDateTime = () => { + this.setState(prevState => ({ openDateTime: prevState.openDateTime !== 'dateTime' ? 'dateTime' : '' })); + } toggleTime = () => { this.setState(prevState => ({ open: prevState.open !== 'time' ? 'time' : '' })); } @@ -245,7 +311,7 @@ class DateTimePicker extends Component { } close = () => { - this.setState({ open: '' }); + this.setState({ open: '', openDateTime: '' }); } open = () => { @@ -312,7 +378,7 @@ class DateTimePicker extends Component { } handleCalendarChange = value => { - const date = setTime(value, this.state.date || new Date()); + const date = setTime(value, this.state.date || new Date(value)); const inputValue = this.format(date); this.setState({ date, inputValue, open: '' }); this.props.onChange(date, `${this.state.operator}${inputValue}`); diff --git a/web/client/themes/default/less/featuregrid.less b/web/client/themes/default/less/featuregrid.less index da98f7ca08..96c24e011c 100644 --- a/web/client/themes/default/less/featuregrid.less +++ b/web/client/themes/default/less/featuregrid.less @@ -87,8 +87,32 @@ } div.rw-datetimepicker.rw-widget span.rw-select{ background: white; + height:30px; } + div.range-tab:hover{ + cursor:pointer; + } + input.rw-input.has-error{ + outline: 2px auto var(--ms-danger, @ms-danger); + } + div.rw-datetimepicker.rw-widget.time-type, div.rw-datetimepicker.rw-widget.rw-has-both{ + div.rw-popup-container.rw-hours-popup{ + right: 0; + left: 0; + > div.rw-popup.rw-widget{ + left:0; + right:0; + } + } + } + div.rw-datetimepicker.rw-widget:not(.rw-datetimepicker.range-time-input.rw-widget){ + div.rw-popup-container.rw-calendar-popup:not(.rw-hours-popup){ + right: -12px; + left:auto; + } + } } + }