diff --git a/web/client/components/data/featuregrid/enhancers/editor.js b/web/client/components/data/featuregrid/enhancers/editor.js index ba284da190..55987eb551 100644 --- a/web/client/components/data/featuregrid/enhancers/editor.js +++ b/web/client/components/data/featuregrid/enhancers/editor.js @@ -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' @@ -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; diff --git a/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx index ed073adfac..640191647d 100644 --- a/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx @@ -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 = { @@ -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 = { @@ -33,7 +37,46 @@ 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) { @@ -41,7 +84,19 @@ class AttributeFilter extends React.PureComponent { } const placeholder = getMessageById(this.context.messages, this.props.placeholderMsgId) || "Search"; let inputKey = 'header-filter-' + this.props.column.key; - return (); + 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 (
+ +
); } renderTooltip = (cmp) => { if (this.props.tooltipMsgId && getMessageById(this.context.messages, this.props.tooltipMsgId)) { @@ -51,19 +106,67 @@ class AttributeFilter extends React.PureComponent { } return cmp; } - + renderOperatorField = () => { + return ( + { + // 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 ( -
- {this.renderTooltip(this.renderInput())} +
+ {this.props.isWithinAttrTbl ? <> + {this.renderOperatorField()} + {['time', 'date', 'date-time'].includes(this.props.type) ? this.renderInput() : this.renderTooltip(this.renderInput())} + : this.renderTooltip(this.renderInput())}
); } 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}); + } } } diff --git a/web/client/components/data/featuregrid/filterRenderers/BaseDateTimeFilter.js b/web/client/components/data/featuregrid/filterRenderers/BaseDateTimeFilter.js index eccde3b3e9..cb3ac224fd 100644 --- a/web/client/components/data/featuregrid/filterRenderers/BaseDateTimeFilter.js +++ b/web/client/components/data/featuregrid/filterRenderers/BaseDateTimeFilter.js @@ -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'; @@ -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 = { @@ -45,6 +52,7 @@ class DateFilter extends AttributeFilter { if (this.props.column.filterable === false) { return ; } + 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`; @@ -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 ( + this.handleChangeRangeFilter(date, stringDate, order)} + /> + ); + } return ( 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 }); } } diff --git a/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx index 8dd637fa25..591245f0f7 100644 --- a/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx @@ -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({ diff --git a/web/client/components/data/featuregrid/filterRenderers/DefaultFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/DefaultFilter.jsx index e168fa3df0..73c957738d 100644 --- a/web/client/components/data/featuregrid/filterRenderers/DefaultFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/DefaultFilter.jsx @@ -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 }); diff --git a/web/client/components/data/featuregrid/filterRenderers/NumberFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/NumberFilter.jsx index 32c51dcc85..0dd2e5a652 100644 --- a/web/client/components/data/featuregrid/filterRenderers/NumberFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/NumberFilter.jsx @@ -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"); @@ -31,7 +31,7 @@ export default compose( props.onChange({ value: isNaN(newVal) ? undefined : newVal, rawValue: value, - operator, + operator: inputOperator || operator, type: 'number', attribute }); @@ -48,7 +48,7 @@ export default compose( isValid && props.onChange({ value, rawValue: value, - operator: "=", + operator: inputOperator || "=", type: 'number', attribute }); diff --git a/web/client/components/data/featuregrid/filterRenderers/StringFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/StringFilter.jsx index 82f3ca29ce..adb13f44e3 100644 --- a/web/client/components/data/featuregrid/filterRenderers/StringFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/StringFilter.jsx @@ -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 }); diff --git a/web/client/components/data/featuregrid/filterRenderers/__tests__/AttributeFilter-test.jsx b/web/client/components/data/featuregrid/filterRenderers/__tests__/AttributeFilter-test.jsx index 0ceb08193d..d309eef0dd 100644 --- a/web/client/components/data/featuregrid/filterRenderers/__tests__/AttributeFilter-test.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/__tests__/AttributeFilter-test.jsx @@ -68,4 +68,22 @@ describe('Test for AttributeFilter component', () => { ReactTestUtils.Simulate.change(input); expect(spyonChange).toHaveBeenCalled(); }); + it('test rendering with operator DD', () => { + const cmp = ReactDOM.render(, document.getElementById("container")); + const el = document.getElementsByClassName("form-control input-sm")[0]; + expect(el).toExist(); + const input = ReactTestUtils.findRenderedDOMComponentWithTag(cmp, "input"); + expect(input.value).toBe("TEST"); + const operatorDropdownListEl = ReactTestUtils.findRenderedDOMComponentWithClass(cmp, 'rw-dropdownlist'); + expect(operatorDropdownListEl).toExist(); + }); + it('test rendering without operator DD', () => { + const cmp = ReactDOM.render(, document.getElementById("container")); + const el = document.getElementsByClassName("form-control input-sm")[0]; + expect(el).toExist(); + const input = ReactTestUtils.findRenderedDOMComponentWithTag(cmp, "input"); + expect(input.value).toBe("TEST"); + const operatorDropdownListEl = document.getElementsByClassName('rw-dropdownlist'); + expect(operatorDropdownListEl.length).toEqual(0); + }); }); diff --git a/web/client/components/data/featuregrid/filterRenderers/__tests__/BaseDateTimeFilter-test.jsx b/web/client/components/data/featuregrid/filterRenderers/__tests__/BaseDateTimeFilter-test.jsx index 3381a61fff..b1f0e5c1de 100644 --- a/web/client/components/data/featuregrid/filterRenderers/__tests__/BaseDateTimeFilter-test.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/__tests__/BaseDateTimeFilter-test.jsx @@ -53,4 +53,24 @@ describe('Test for BaseDateTimeFilter component', () => { expect(el).toExist(); }); + it('render with range operator ><', () => { + // for type date + ReactDOM.render(<'}} isWithinAttrTbl={"true"} />, document.getElementById("container")); + let el = document.getElementsByTagName("input")[0]; + expect(el).toExist(); + let dateTimePickerWithRangeElement = document.getElementsByClassName('rw-datetimepicker range-time-input rw-widget')[0]; + expect(dateTimePickerWithRangeElement).toExist(); + // for time date + ReactDOM.render(<'}} isWithinAttrTbl={"true"} />, document.getElementById("container")); + el = document.getElementsByTagName("input")[0]; + expect(el).toExist(); + dateTimePickerWithRangeElement = document.getElementsByClassName('rw-datetimepicker range-time-input rw-widget')[0]; + expect(dateTimePickerWithRangeElement).toExist(); + // for type date-time + ReactDOM.render(<'}} isWithinAttrTbl={"true"} />, document.getElementById("container")); + el = document.getElementsByTagName("input")[0]; + expect(el).toExist(); + dateTimePickerWithRangeElement = document.getElementsByClassName('rw-datetimepicker range-time-input rw-widget')[0]; + expect(dateTimePickerWithRangeElement).toExist(); + }); }); 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/data/featuregrid/filterRenderers/__tests__/index-test.jsx b/web/client/components/data/featuregrid/filterRenderers/__tests__/index-test.jsx index a3c233457b..69e15c224a 100644 --- a/web/client/components/data/featuregrid/filterRenderers/__tests__/index-test.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/__tests__/index-test.jsx @@ -66,4 +66,56 @@ describe('Test for filterRenderer function', () => { unregisterFilterRenderer("test"); } }); + it('render filter components for attribute table', () => { + // default filter + let Cmp = getFilterRenderer({type: "unknown", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + let operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + let input = document.getElementsByClassName("form-control input-sm")[0]; + expect(operatorDropdownEl).toExist(); + expect(input).toExist(); + // string filter + Cmp = getFilterRenderer({type: "string", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + input = document.getElementsByClassName("form-control input-sm")[0]; + expect(operatorDropdownEl).toExist(); + expect(input).toExist(); + // number filter + Cmp = getFilterRenderer({type: "int", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + input = document.getElementsByClassName("form-control input-sm")[0]; + expect(operatorDropdownEl).toExist(); + expect(input).toExist(); + // number filter + Cmp = getFilterRenderer({type: "number", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + input = document.getElementsByClassName("form-control input-sm")[0]; + expect(operatorDropdownEl).toExist(); + expect(input).toExist(); + // time filter + Cmp = getFilterRenderer({type: "time", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + expect(operatorDropdownEl).toExist(); + // date filter + Cmp = getFilterRenderer({type: "date", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + expect(operatorDropdownEl).toExist(); + // date-time filter + Cmp = getFilterRenderer({type: "date-time", isWithinAttrTbl: true}); + expect(Cmp).toExist(); + ReactDOM.render(, document.getElementById("container")); + operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0]; + expect(operatorDropdownEl).toExist(); + }); }); diff --git a/web/client/components/data/featuregrid/filterRenderers/index.js b/web/client/components/data/featuregrid/filterRenderers/index.js index c41567041f..3f6cc8ea42 100644 --- a/web/client/components/data/featuregrid/filterRenderers/index.js +++ b/web/client/components/data/featuregrid/filterRenderers/index.js @@ -15,13 +15,41 @@ import NumberFilter from './NumberFilter'; import StringFilter from './StringFilter'; const types = { - "defaultFilter": (type) => withProps(() =>({type: type}))(DefaultFilter), - "string": () => StringFilter, - "number": () => NumberFilter, - "int": () => NumberFilter, - "date": () => withProps(() =>({type: "date"}))(DateTimeFilter), - "time": () => withProps(() =>({type: "time"}))(DateTimeFilter), - "date-time": () => withProps(() =>({type: "date-time"}))(DateTimeFilter), + "defaultFilter": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.default" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.default" : ""; + return { type: props.type, isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(DefaultFilter), + "string": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.string" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.string" : ""; + return { type: 'string', isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(StringFilter), + "number": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.number" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.number" : ""; + return { type: 'number', isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(NumberFilter), + "int": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.number" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.number" : ""; + return { type: 'integer', isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(NumberFilter), + "date": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.date" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.date" : ""; + return { type: "date", isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(DateTimeFilter), + "time": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.date" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.date" : ""; + return { type: "time", isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(DateTimeFilter), + "date-time": (props) => withProps(() =>{ + let placeholderMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.placeholders.date" : ''; + let tooltipMsgId = props.isWithinAttrTbl ? "featuregrid.attributeFilter.tooltips.date" : ""; + return { type: "date-time", isWithinAttrTbl: props.isWithinAttrTbl || false, placeholderMsgId, tooltipMsgId }; + })(DateTimeFilter), "geometry": () => GeometryFilter }; @@ -46,11 +74,11 @@ export const getFilterRendererByName = (name) => { * @param {string} [params.type] the type of the filter renderer. The available types are: "defaultFilter", "string", "number", "int", "date", "time", "date-time", "geometry". * @returns {React.Component} the filter renderer */ -export const getFilterRenderer = ({name, type}) => { +export const getFilterRenderer = ({name, type, isWithinAttrTbl}) => { if (name) { return getFilterRendererByName(name); } - return types[type] ? types[type](type) : types.defaultFilter(type); + return types[type] ? types[type]({type, isWithinAttrTbl}) : types.defaultFilter({type, isWithinAttrTbl}); }; diff --git a/web/client/components/misc/datetimepicker/DateTimePicker.js b/web/client/components/misc/datetimepicker/DateTimePicker.js index 1a58d8b8db..22d0f82524 100644 --- a/web/client/components/misc/datetimepicker/DateTimePicker.js +++ b/web/client/components/misc/datetimepicker/DateTimePicker.js @@ -12,10 +12,12 @@ import PropTypes from 'prop-types'; import moment from 'moment'; import { Calendar } from 'react-widgets'; import localizer from 'react-widgets/lib/localizers/moment'; -import { Tooltip } from 'react-bootstrap'; -import { isDate, isNil } from 'lodash'; +import { Tooltip, Glyphicon } from 'react-bootstrap'; +import { isDate, isNil, omit } from 'lodash'; import OverlayTrigger from '../OverlayTrigger'; import Hours from './Hours'; +import Popover from '../../styleeditor/Popover'; +import {getMessageById} from '../../../utils/LocaleUtils'; localizer(moment); @@ -66,16 +68,21 @@ class DateTimePicker extends Component { culture: PropTypes.string, toolTip: PropTypes.string, tabIndex: PropTypes.string, - options: PropTypes.object + options: PropTypes.object, + isWithinAttrTbl: PropTypes.bool } - + static contextTypes = { + messages: PropTypes.object, + locale: PropTypes.string + }; static defaultProps = { placeholder: 'Type date...', calendar: true, time: true, onChange: () => { }, value: null, - popupPosition: 'bottom' + popupPosition: 'bottom', + isWithinAttrTbl: false } state = { @@ -95,6 +102,7 @@ class DateTimePicker extends Component { if (prevProps.value !== this.props.value || prevProps.operator !== this.props.operator) { const { value, operator } = this.props; this.setDateFromValueProp(value, operator); + if (this.props.operator === 'isNull') this.setState({ inputValue: '', date: null }); } } @@ -103,19 +111,61 @@ class DateTimePicker extends Component { const { date: dateFormat, time: timeFormat, base: defaultFormat } = formats; return format ? format : !time && calendar ? dateFormat : time && !calendar ? timeFormat : defaultFormat; } + renderCustomDateTimePopup = () => { + const { inputValue, operator, open } = this.state; + const { tabIndex, type } = this.props; + + const timeVisible = open === 'time'; + const props = omit(this.props, ['placeholder', 'calendar', 'time', 'onChange', 'value']); + const calendarVal = this.props.value?.startDate ?? this.props.value; + let timePlaceholderMsgId = getMessageById(this.context.messages, "featuregrid.attributeFilter.placeholders.time"); - renderInput = (inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible) => { + return ( +
+
+
+ {this.attachCalRef = elem;}} + onMouseDown={this.handleMouseDown} + onChange={this.handleCalendarChange} + {...props} + value={!isNil(calendarVal) ? new Date(calendarVal) : undefined} + /> +
+
+ {this.renderInput(inputValue, operator, '', timePlaceholderMsgId, tabIndex, false, true, 'form-control')} + + + +
+
+ {this.attachTimeRef = elem; }} value={inputValue} {...props} onClose={this.close} onSelect={(time) => this.handleTimeSelect(time, type)} /> +
+
+
+
+
+ ); + }; + renderInput = (inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible, className) => { + let inputV = this.props.isWithinAttrTbl ? `${inputValue}` : `${operator}${inputValue}`; + let isNullOperator = this.props.operator === 'isNull'; + if (isNullOperator) inputV = ''; + const inputEl = ; if (toolTip) { return ({toolTip}}> - + {inputEl} ); } - return (); + return inputEl; } 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, type, popupPosition } = 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 @@ -127,42 +177,89 @@ class DateTimePicker extends Component { }, {}); const calendarVisible = open === 'date'; const timeVisible = open === 'time'; - + const dateTimeVisible = openDateTime === 'dateTime'; + const calendarVal = this.props.value?.startDate ?? this.props.value; + let timePlaceholderMsgId = getMessageById(this.context.messages, "featuregrid.attributeFilter.placeholders.time"); + + if (type === 'date-time') { + return (
{this.dateTimeRef = elem;}} onBlur={() => this.handleWidgetBlur(type)} onKeyDown={this.handleKeyDown} onFocus={this.handleWidgetFocus} className={`rw-datetimepicker range-time-input rw-widget ${focused ? 'rw-state-focus' : ''}`}> + {this.renderInput(inputValue, operator, dateTimeVisible ? '' : toolTip, placeholder, tabIndex, true, true)} + + + {this.renderCustomDateTimePopup()} +
+ } + > + + +
+
); + } else if (type === 'time') { + return ( +
+ {this.renderInput(inputValue, operator, timeVisible ? '' : toolTip, timePlaceholderMsgId, tabIndex, calendarVisible, timeVisible)} + + +
+
+ { this.attachTimeRef = elem;}} value={inputValue} onMouseDown={this.handleMouseDown} {...props} onClose={this.close} onSelect={this.handleTimeSelect} /> +
+
+
+ } + > + + + + + ); + } return ( -
- {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible)} - {calendar || time ? +
+ {this.renderInput(inputValue, operator, calendarVisible ? '' : toolTip, placeholder, tabIndex, calendarVisible, timeVisible)} + {calendar ? - { - calendar ?
+ } + > + : '' - } - { - time ? : '' - } + + : '' } -
-
- -
-
-
-
- -
-
); } @@ -176,11 +273,16 @@ class DateTimePicker extends Component { this.ignoreBlur = false; } - handleWidgetBlur = () => { + handleWidgetBlur = (type) => { if (this.ignoreBlur) { return; } - this.setState({ open: '', focused: false }); + if (type === 'date-time') { + // this.dateTimeRef.click(); + this.setState({ openDateTime: '', focused: false }); + } else { + this.setState({ open: '', focused: false }); + } } handleMouseDown = () => { @@ -190,7 +292,9 @@ class DateTimePicker extends Component { toggleCalendar = () => { this.setState(prevState => ({ open: prevState.open !== 'date' ? 'date' : '' })); } - + toggleDateTime = () => { + this.setState(prevState => ({ openDateTime: prevState.openDateTime !== 'dateTime' ? 'dateTime' : '', open: '' })); + } toggleTime = () => { this.setState(prevState => ({ open: prevState.open !== 'time' ? 'time' : '' })); } @@ -236,7 +340,7 @@ class DateTimePicker extends Component { } close = () => { - this.setState({ open: '' }); + this.setState({ open: '', openDateTime: '' }); } open = () => { @@ -273,11 +377,11 @@ class DateTimePicker extends Component { } if (timeVisible) { - this.timeRef.handleKeyDown(e); + this.timeRef?.handleKeyDown(e); } if (calVisible) { - this.calRef.refs.inner.handleKeyDown(e); + this.calRef?.refs?.inner?.handleKeyDown(e); } if (!timeVisible && !calVisible && e.key === 'Enter') { @@ -303,13 +407,16 @@ 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}`); } - handleTimeSelect = time => { + handleTimeSelect = (time, pickerType) => { + if (pickerType === 'date-time') { + this.ignoreBlur = true; + } const selectedDate = this.state.date || new Date(); const date = setTime(selectedDate, time.date); const inputValue = this.format(date); diff --git a/web/client/components/misc/datetimepicker/Hours.js b/web/client/components/misc/datetimepicker/Hours.js index 6db0a025bb..0844b0d455 100644 --- a/web/client/components/misc/datetimepicker/Hours.js +++ b/web/client/components/misc/datetimepicker/Hours.js @@ -1,6 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; +import { getLocalTimePart } from '../../../utils/TimeUtils'; const getDates = (step) => { let min = moment().startOf('day'); @@ -23,15 +24,21 @@ class Hours extends Component { static propTypes = { onSelect: PropTypes.func, onMouseDown: PropTypes.func, - disabled: PropTypes.bool + disabled: PropTypes.bool, + style: PropTypes.object, + value: PropTypes.string, + type: PropTypes.string } static defaultProps = { onSelect: () => { }, onMouseDown: () => {}, - disabled: false + disabled: false, + style: {}, + value: '', + type: '' } - state = { focusedItemIndex: 0, times: [] }; + state = { focusedItemIndex: -1, times: [] }; componentDidMount() { this.setState({ times: getDates() }); @@ -39,10 +46,11 @@ class Hours extends Component { render() { const { focusedItemIndex, times } = this.state; - const { onMouseDown, onSelect, disabled } = this.props; + const { onMouseDown, onSelect, disabled, style, value, type } = this.props; + let selectedVal = type === 'date-time' ? (value?.split(" ")[1] || "") : value; // in case of date-time --> extract hours from selected passed value ex.: 01/01/2024 10:00:00 return ( -