diff --git a/web/client/actions/featuregrid.js b/web/client/actions/featuregrid.js index 5d13667dfa..198a59c5b2 100644 --- a/web/client/actions/featuregrid.js +++ b/web/client/actions/featuregrid.js @@ -30,6 +30,7 @@ export const SET_FEATURES = 'SET_FEATURES'; export const SORT_BY = 'FEATUREGRID:SORT_BY'; export const SET_LAYER = 'FEATUREGRID:SET_LAYER'; export const UPDATE_FILTER = 'QUERY:UPDATE_FILTER'; +export const UPDATE_OPERATOR_QUICK_FILTER = 'UPDATE_OPERATOR_QUICK_FILTER'; export const CHANGE_PAGE = 'FEATUREGRID:CHANGE_PAGE'; export const GEOMETRY_CHANGED = 'FEATUREGRID:GEOMETRY_CHANGED'; export const DOCK_SIZE_FEATURES = 'DOCK_SIZE_FEATURES'; @@ -234,6 +235,13 @@ export function updateFilter(update, append = false) { append }; } +export function updateOperatorQuickFilter(operator, attribute) { + return { + type: UPDATE_FILTER, + operator, + attribute + }; +} export function toggleTool(tool, value) { return { type: TOGGLE_TOOL, diff --git a/web/client/components/data/featuregrid/enhancers/editor.js b/web/client/components/data/featuregrid/enhancers/editor.js index ba284da190..fe0076ca7f 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, + isShownOperators: 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}), + isShownOperators: props.isShownOperators })) }); return result; diff --git a/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/AttributeFilter.jsx index ed073adfac..b559e4b2d2 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, + isShownOperators: PropTypes.bool }; static contextTypes = { @@ -33,7 +37,45 @@ class AttributeFilter extends React.PureComponent { valid: true, onChange: () => {}, column: {}, - placeholderMsgId: "featuregrid.filter.placeholders.default" + placeholderMsgId: "featuregrid.filter.placeholders.default", + operator: "=", + isShownOperators: false + }; + constructor(props) { + super(props); + this.state = { + listOperators: ["="], + stringOperators: ["=", "<>", "like", "ilike", "isNull"], + arrayOperators: ["contains"], + booleanOperators: ["="], + defaultOperators: ["=", ">", "<", ">=", "<=", "<>", "isNull"], + timeDateOperators: ["=", ">", "<", ">=", "<=", "<>", "><", "isNull"], + operator: this.props.operator || "=" + }; + } + 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 +83,16 @@ class AttributeFilter extends React.PureComponent { } const placeholder = getMessageById(this.context.messages, this.props.placeholderMsgId) || "Search"; let inputKey = 'header-filter-' + this.props.column.key; - return (); + return (
+ +
); } renderTooltip = (cmp) => { if (this.props.tooltipMsgId && getMessageById(this.context.messages, this.props.tooltipMsgId)) { @@ -51,11 +102,30 @@ class AttributeFilter extends React.PureComponent { } return cmp; } - + renderOperatorField = () => { + return ( + { + this.setState({ operator: selectedOperator, value: selectedOperator === 'isNull' ? undefined : this.state?.value ?? this.props.value }); + let isNullOperatorSelected = selectedOperator === 'isNull'; + let isValueExist = this.state?.value ?? this.props.value; + let isOperatorChangedFromIsNullAndValueNotExist = this.state.operator === 'isNull' && this.state.operator !== selectedOperator && !isValueExist; + if (isValueExist || isNullOperatorSelected || isOperatorChangedFromIsNullAndValueNotExist ) this.props.onChange({value: this.state?.value ?? this.props.value, attribute: this.props.column && this.props.column.key, inputOperator: selectedOperator}); + }} + fieldValue={this.state.operator} + onUpdateField={this.updateFieldElement}/> + ); + }; render() { let inputKey = 'header-filter--' + this.props.column.key; return (
+ {this.props.isShownOperators ? this.renderOperatorField() : null} {this.renderTooltip(this.renderInput())}
); @@ -63,7 +133,7 @@ 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}); + 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..706baaf8dc 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 CustomDateTimePickerWithRange from '../../../misc/datetimepicker/CustomDateTimePickerWithRange'; 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" +})(CustomDateTimePickerWithRange); + 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,7 +66,23 @@ 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.handleChange(date, stringDate)} + /> + ); + } return (); } 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 }); } } diff --git a/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx b/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx index 8dd637fa25..311b6ecc61 100644 --- a/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx +++ b/web/client/components/data/featuregrid/filterRenderers/DateTimeFilter.jsx @@ -15,7 +15,7 @@ export default compose( value: null }), withHandlers({ - onChange: props => ({ value, attribute, stringValue } = {}) => { + onChange: props => ({ value, attribute, stringValue, inputOperator } = {}) => { const match = /\s*(!==|!=|<>|<=|>=|===|==|=|<|>)?(.*)/.exec(stringValue); const operator = match[1]; let enhancedOperator = match[1] || '='; @@ -28,7 +28,7 @@ export default compose( props.onValueChange(value); props.onChange({ value: { startDate: value, operator }, - operator: enhancedOperator, + operator: inputOperator || enhancedOperator, type: props.type, attribute }); 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..b4caf1085f 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", // need to read operator from redux beased on operator selected option type: 'string', attribute }); diff --git a/web/client/components/data/featuregrid/filterRenderers/index.js b/web/client/components/data/featuregrid/filterRenderers/index.js index c41567041f..e69bc7be26 100644 --- a/web/client/components/data/featuregrid/filterRenderers/index.js +++ b/web/client/components/data/featuregrid/filterRenderers/index.js @@ -15,13 +15,13 @@ 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(() =>({type: props.type, isShownOperators: props.isShownOperators || false}))(DefaultFilter), + "string": (props) => withProps(() =>({type: 'string', isShownOperators: props.isShownOperators || false}))(StringFilter), + "number": (props) => withProps(() =>({type: 'number', isShownOperators: props.isShownOperators || false}))(NumberFilter), + "int": (props) => withProps(() =>({type: 'integer', isShownOperators: props.isShownOperators || false}))(NumberFilter), + "date": (props) => withProps(() =>({type: "date", isShownOperators: props.isShownOperators || false}))(DateTimeFilter), + "time": (props) => withProps(() =>({type: "time", isShownOperators: props.isShownOperators || false}))(DateTimeFilter), + "date-time": (props) => withProps(() =>({type: "date-time", isShownOperators: props.isShownOperators || false}))(DateTimeFilter), "geometry": () => GeometryFilter }; @@ -46,11 +46,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, isShownOperators}) => { if (name) { return getFilterRendererByName(name); } - return types[type] ? types[type](type) : types.defaultFilter(type); + return types[type] ? types[type]({type, isShownOperators}) : types.defaultFilter({type, isShownOperators}); }; diff --git a/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js b/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js new file mode 100644 index 0000000000..de6c98379f --- /dev/null +++ b/web/client/components/misc/datetimepicker/CustomDateTimePickerWithRange.js @@ -0,0 +1,591 @@ +/* + * Copyright 2019, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React, { Component } from 'react'; + +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 OverlayTrigger from '../OverlayTrigger'; +import Hours from './Hours'; + +localizer(moment); + +// lang is supported by moment < 2.8.0 in favour of locale +const localField = typeof moment().locale === 'function' ? 'locale' : 'lang'; + +function getMoment(culture, value, format) { + return culture ? moment(value, format)[localField](culture) : moment(value, format); +} + +const setTime = (date, dateWithTime) => { + const value = moment(date); + value.hours(dateWithTime.getHours()) + .minute(dateWithTime.getMinutes()) + .seconds(dateWithTime.getSeconds()) + .milliseconds(dateWithTime.getMilliseconds()); + return value.toDate(); +}; + +const formats = { + base: 'lll', + date: 'L', + time: 'LT' +}; + +/** + * @name DateTimePicker + * 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/. + * The operator supported must be placed before date in input field and it should be + * one of !==|!=|<>|<=|>=|===|==|=|<|> operator. Anything else should not be + * considered as operator by this component. + * + */ +class DateTimePicker extends Component { + + static propTypes = { + format: PropTypes.string, + type: PropTypes.string, + placeholder: PropTypes.string, + onChange: PropTypes.func, + calendar: PropTypes.bool, + popupPosition: PropTypes.oneOf(['top', 'bottom']), + time: PropTypes.bool, + value: PropTypes.any, + operator: PropTypes.string, + culture: PropTypes.string, + toolTip: PropTypes.string, + tabIndex: PropTypes.string, + options: PropTypes.object + } + + static defaultProps = { + placeholder: 'Type date...', + calendar: true, + time: true, + onChange: () => { }, + value: null, + popupPosition: 'bottom' + } + + state = { + openRangeContainer: false, + openRangeInputs: 'start', // start, end + openDateTCalendar: false, // start, end + openDateTTime: false, // start, end + focused: false, + inputValue: '', + operator: '', + date: null + } + + componentDidMount() { + const { value, operator } = this.props; + this.setDateFromValueProp(value, operator); + } + + componentDidUpdate(prevProps) { + if (prevProps.value !== this.props.value || prevProps.operator !== this.props.operator) { + const { value, operator } = this.props; + this.setDateFromValueProp(value, operator); + } + } + + getFormat = () => { + const { format, time, calendar } = this.props; + const { date: dateFormat, time: timeFormat, base: defaultFormat } = formats; + return format ? format : !time && calendar ? dateFormat : time && !calendar ? timeFormat : defaultFormat; + } + + renderInput = (inputValue, operator, toolTip, placeholder, tabIndex, calendarVisible, timeVisible, disabled, isFullWidth) => { + if (toolTip) { + return ({toolTip}}> + + ); + } + return (); + } + renderHoursRange = () =>{ + const { inputValue, operator, focused, openRangeInputs } = this.state; + const { toolTip, placeholder, tabIndex } = 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 + return acc; + } + acc[key] = this.props[key]; + return acc; + + }, {}); + // const calendarVisible = open === 'date'; + // const timeVisible = open === 'time'; + return ( +
+
+
+ Start + + Please Enter ... + +
+
+ End + + Please Enter ... + +
+
+
+
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, false, true)} + + + +
+ +
+
+
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, false, true)} + + + +
+ +
+
+
+
+ ); + } + renderHours = () =>{ + const { inputValue, operator, focused, openRangeContainer } = this.state; + const { toolTip, placeholder, tabIndex, 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 + // return acc; + // } + // acc[key] = this.props[key]; + // return acc; + + // }, {}); + // const calendarVisible = open === 'date'; + // const timeVisible = open === 'time'; + return ( +
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, false, true, true)} + + + + { openRangeContainer && <> +
+ {this.renderHoursRange()} +
+ } + +
+ ); + } + renderCalendarRange = () =>{ + const { openRangeInputs } = this.state; + // const { 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 + return acc; + } + acc[key] = this.props[key]; + return acc; + + }, {}); + // const calendarVisible = type === 'date'; + // const timeVisible = open === 'time'; + return ( +
+
+
+ Start + + Please Enter ... + +
+
+ End + + Please Enter ... + +
+
+
+
+ +
+
+ +
+
+
+ ); + } + renderCalendar = () =>{ + const { inputValue, operator, focused, openRangeContainer, popupPosition } = this.state; + const { toolTip, placeholder, tabIndex } = 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 + // return acc; + // } + // acc[key] = this.props[key]; + // return acc; + + // }, {}); + // const calendarVisible = type === 'date'; + // const timeVisible = open === 'time'; + return ( +
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, true, false, true)} + + + + { openRangeContainer && <> +
+ { this.renderCalendarRange() } +
+ } +
+ ); + } + renderDateTimeRange = () =>{ + const { inputValue, operator, openRangeInputs, openDateTTime } = this.state; + const { toolTip, placeholder, tabIndex } = this.props; + + // const { 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 + return acc; + } + acc[key] = this.props[key]; + return acc; + + }, {}); + // const calendarVisible = type === 'date'; + // const timeVisible = open === 'time'; + return ( +
+
+
+ Start + + Please Enter ... + +
+
+ End + + Please Enter ... + +
+
+
+
+ +
+
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, false, true, false, true)} + + + +
+
+ +
+
+
+
+ +
+
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, false, true, false, true)} + + + +
+
+ +
+
+
+
+
+ ); + } + renderCalendarTimeDate = () =>{ + const { openRangeContainer, inputValue, operator, focused } = this.state; + const { toolTip, placeholder, tabIndex, 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 + // return acc; + // } + // acc[key] = this.props[key]; + // return acc; + + // }, {}); + // const calendarVisible = open === 'date'; + // const timeVisible = open === 'time'; + return ( +
+ {this.renderInput(inputValue, operator, toolTip, placeholder, tabIndex, true, true, true)} + + + + { openRangeContainer && <> +
+ {this.renderDateTimeRange()} +
+ } +
+ ); + } + + render() { + // const { open } = this.state; + const { 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 + // return acc; + // } + // acc[key] = this.props[key]; + // return acc; + + // }, {}); + // const calendarVisible = open === 'date'; + // const timeVisible = open === 'time'; + if (type === 'time') return this.renderHours(); + else if (type === 'date') return this.renderCalendar(); + return this.renderCalendarTimeDate(); + } + + inputFlush = false; + // Ignore blur to manual control de-rendering of cal/time popup + ignoreBlur = false; + + handleWidgetFocus = () => { + this.setState({ focused: true }); + this.ignoreBlur = false; + } + + handleWidgetBlur = () => { + if (this.ignoreBlur) { + return; + } + this.setState({ open: '', focused: false }); + } + rangeContainerMouseLeaveHandler = () => { + this.setState({ openRangeContainer: false }); + } + handleMouseDown = () => { + this.ignoreBlur = true; + } + + toggleStart = () => { + if (this.state.openRangeInputs !== 'start') { + this.setState({ openRangeInputs: 'start', openDateTTime: false, openDateTCalendar: false }); + } + } + toggleEnd = () => { + if (this.state.openRangeInputs !== 'end') { + this.setState({ openRangeInputs: 'end', openDateTTime: false, openDateTCalendar: false }); + } + } + toggleTimeInDateTime = () => { + this.setState(prev => ({ openDateTTime: !prev.openDateTTime })); + } + toggleHandler = () => { + this.setState(prevState => ({ openRangeContainer: !prevState.openRangeContainer, openDateTTime: false, openDateTCalendar: false })); + } + + handleInputBlur = () => { + if (this.inputFlush) { + // date has changed + const parsed = this.parse(this.state.inputValue); + const dateStr = this.format(parsed); + this.setState({ + inputValue: dateStr, + date: parsed + }); + this.inputFlush = false; + this.props.onChange(parsed, `${this.state.operator}${dateStr}`); + } + } + + setDateFromValueProp = (value, operator) => { + if (isDate(value)) { + const inputValue = this.format(value); + this.setState(prevState => ({ date: value, inputValue, operator: operator || prevState.operator })); + } + } + + parse = (value) => { + const { culture } = this.props; + const format = this.getFormat(); + if (value) { + const m = getMoment(culture, value, format); + if (m.isValid()) return m.toDate(); + } + return null; + } + + format = (value) => { + if (!value) return ''; + const { culture } = this.props; + const format = this.getFormat(); + const m = getMoment(culture, value); + if (m.isValid()) return m.format(format); + return ''; + } + + close = () => { + this.setState({ open: '' }); + } + + openHandler = () => { + const { calendar, time } = this.props; + return !calendar && time ? this.setState({ open: 'time' }) : calendar && !time ? this.setState({ open: 'date' }) : ''; + } + + handleKeyDown = e => { + const { open } = this.state; + const timeVisible = open === 'time'; + const calVisible = open === 'date'; + + if (e.defaultPrevented) return; + + if (e.key === 'Escape') { + // escape key should close the calendar or time popup + this.close(); + return; + } + if (e.altKey && e.key === 'ArrowDown') { + // user press control/option key for mac together with arrow down + // this should open the popup + e.preventDefault(); + this.open(); + return; + } + + if (e.altKey && e.key === 'ArrowUp') { + // user press control/option key for mac together with arrow up + // this should close the popup + e.preventDefault(); + this.close(); + return; + } + + if (timeVisible) { + this.timeRef.handleKeyDown(e); + } + + if (calVisible) { + this.calRef.refs.inner.handleKeyDown(e); + } + + if (!timeVisible && !calVisible && e.key === 'Enter') { + // enter key is pressed while hours and calendar are not visible + // date has changed + const parsed = this.parse(this.state.inputValue); + const dateStr = this.format(parsed); + this.setState({ + inputValue: dateStr, + date: parsed + }); + this.inputFlush = false; + this.props.onChange(parsed, `${this.state.operator}${dateStr}`); + } + } + + + handleValueChange = (event) => { + const { value } = event.target; + const match = /\s*(!==|!=|<>|<=|>=|===|==|=|<|>)?(.*)/.exec(value); + this.setState({ inputValue: match[2], operator: match[1] || '' }); + this.inputFlush = true; + } + + handleCalendarChange = value => { + const date = setTime(value, this.state.date || new Date()); + const inputValue = this.format(date); + this.setState({ date, inputValue, open: '' }); + this.props.onChange(date, `${this.state.operator}${inputValue}`); + } + + handleTimeSelect = time => { + const selectedDate = this.state.date || new Date(); + const date = setTime(selectedDate, time.date); + const inputValue = this.format(date); + this.setState({ date, inputValue, open: '' }); + this.props.onChange(date, `${this.state.operator}${inputValue}`); + } + + attachTimeRef = ref => (this.timeRef = ref) + + attachCalRef = ref => (this.calRef = ref) + +} +export default DateTimePicker; + diff --git a/web/client/plugins/featuregrid/FeatureEditor.jsx b/web/client/plugins/featuregrid/FeatureEditor.jsx index 935efab545..65e9ba5fd5 100644 --- a/web/client/plugins/featuregrid/FeatureEditor.jsx +++ b/web/client/plugins/featuregrid/FeatureEditor.jsx @@ -10,7 +10,7 @@ import {connect} from 'react-redux'; import {createSelector, createStructuredSelector} from 'reselect'; import {bindActionCreators} from 'redux'; import { get, pick, isEqual } from 'lodash'; -import {compose, lifecycle} from 'recompose'; +import {compose, lifecycle, defaultProps } from 'recompose'; import ReactDock from 'react-dock'; import ContainerDimensions from 'react-container-dimensions'; @@ -187,7 +187,9 @@ const FeatureDock = (props = { }; const items = props?.items ?? []; const toolbarItems = items.filter(({target}) => target === 'toolbar'); - const filterRenderers = useMemo(() => getFilterRenderers(props.describe, props.fields), [props.describe, props.fields]); + const filterRenderers = useMemo(() => { + return getFilterRenderers(props.describe, props.fields, props.isShownOperators); + }, [props.describe, props.fields]); return (
{ props.onSizeChange(size, dockProps); }}> @@ -209,6 +211,7 @@ const FeatureDock = (props = { footer={getFooter(props)}> {getDialogs(props.tools)} ({}), (dispatch) => ({ onMount: bindActionCreators(setUp, dispatch), diff --git a/web/client/plugins/featuregrid/panels/index.jsx b/web/client/plugins/featuregrid/panels/index.jsx index 3d5dc6f2f5..8009a5083b 100644 --- a/web/client/plugins/featuregrid/panels/index.jsx +++ b/web/client/plugins/featuregrid/panels/index.jsx @@ -193,7 +193,7 @@ export const getEmptyRowsView = () => { * @param {object[]} fields array of fields (with `filterRenderer` property) * @returns {object} object with field name as key and filterRenderer as value */ -export const getFilterRenderers = (describe, fields = []) => { +export const getFilterRenderers = (describe, fields = [], isShownOperators) => { if (describe) { return (getFeatureTypeProperties(describe) || []).reduce( (out, cur) => { const field = fields.find(f => f.name === cur.name); @@ -206,6 +206,7 @@ export const getFilterRenderers = (describe, fields = []) => { (filter, mode) => { const props = { value: filter && (filter.rawValue || filter.value), + operator: filter && (filter.operator), ...(isGeometryType(cur) ? { filterEnabled: filter?.enabled, filterDeactivated: filter?.deactivated @@ -217,7 +218,7 @@ export const getFilterRenderers = (describe, fields = []) => { } : {}; return mode === "EDIT" ? {...props, ...editProps} : props; } - ))(getFilterRenderer({type: isGeometryType(cur) ? 'geometry' : cur.localType, name: field?.filterRenderer?.name, options: field?.filterRenderer?.options})) + ))(getFilterRenderer({type: isGeometryType(cur) ? 'geometry' : cur.localType, name: field?.filterRenderer?.name, options: field?.filterRenderer?.options, isShownOperators})) }; }, {}); } diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js index ebfa22e0e7..b1aecd4a05 100644 --- a/web/client/reducers/featuregrid.js +++ b/web/client/reducers/featuregrid.js @@ -39,6 +39,7 @@ import { OPEN_FEATURE_GRID, CLOSE_FEATURE_GRID, UPDATE_FILTER, + UPDATE_OPERATOR_QUICK_FILTER, INIT_PLUGIN, SIZE_CHANGE, STORE_ADVANCED_SEARCH_FILTER, @@ -367,6 +368,22 @@ function featuregrid(state = emptyResultsState, action) { } return state; } + case UPDATE_OPERATOR_QUICK_FILTER : { + const {attribute, operator} = (action || {}); + if (attribute) { + const filter = state.filters[attribute]; + return assign({}, state, { + filters: { + [attribute]: { + ...filter, + operator: operator, + value: undefined + } + } + }); + } + return state; + } case UPDATE_FILTER : { const {attribute} = (action.update || {}); if (attribute && action.append) { diff --git a/web/client/themes/default/less/featuregrid.less b/web/client/themes/default/less/featuregrid.less index 2d7c22da24..8d0e2f32e1 100644 --- a/web/client/themes/default/less/featuregrid.less +++ b/web/client/themes/default/less/featuregrid.less @@ -59,6 +59,36 @@ } } +// **************** +/*styles related to feature grid columns filters*/ +// **************** +.feature-grid-drag-handle-show{ + div.form-group{ + display:flex; + div.rw-dropdownlist.rw-widget{ + min-width:fit-content; + } + div.rw-popup.rw-widget, div.rw-widget{ + width:100%; + } + div.rw-popup.rw-widget{ + height: 150px; + } + input.form-control.input-sm, div.rw-popup.rw-widget ul.rw-list{ + height:100%; + } + div.rw-widget div.rw-input{ + padding-right: 0.5rem; + } + .range-time-input div.rw-calendar-popup.rw-popup-container{ + left: -2.5rem; + overflow-x:hidden; + overflow-y:auto; + } + } + +} + // ************** // FeatureEditorFallback Layout // ************** diff --git a/web/client/themes/default/less/react-data-grid.less b/web/client/themes/default/less/react-data-grid.less index ab8eafb870..41b7a61774 100644 --- a/web/client/themes/default/less/react-data-grid.less +++ b/web/client/themes/default/less/react-data-grid.less @@ -297,6 +297,10 @@ .rw-datetimepicker.rw-widget input { .input-size(@input-height-small; @padding-small-vertical; @padding-small-horizontal; @font-size-small; @line-height-small; @input-border-radius-small); } + .rw-datetimepicker.rw-widget input:disabled{ + width: auto; + height:100%; + } } } } diff --git a/web/client/utils/FeatureGridUtils.js b/web/client/utils/FeatureGridUtils.js index 19e41a2643..a0fbe91435 100644 --- a/web/client/utils/FeatureGridUtils.js +++ b/web/client/utils/FeatureGridUtils.js @@ -129,11 +129,11 @@ export const featureTypeToGridColumns = ( columnSettings = {}, fields = [], {editable = false, sortable = true, resizable = true, filterable = true, defaultSize = 200, options = []} = {}, - {getEditor = () => {}, getFilterRenderer = () => {}, getFormatter = () => {}, getHeaderRenderer = () => {}} = {}) => + {getEditor = () => {}, getFilterRenderer = () => {}, getFormatter = () => {}, getHeaderRenderer = () => {}, isShownOperators = false} = {}) => getAttributeFields(describe).filter(e => !(columnSettings[e.name] && columnSettings[e.name].hide)).map((desc) => { const option = options.find(o => o.name === desc.name); const field = fields.find(f => f.name === desc.name); - return { + let columnProp = { sortable, key: desc.name, width: columnSettings[desc.name] && columnSettings[desc.name].width || (defaultSize ? defaultSize : undefined), @@ -147,8 +147,10 @@ export const featureTypeToGridColumns = ( filterable, editor: getEditor(desc, field), formatter: getFormatter(desc, field), - filterRenderer: getFilterRenderer(desc, field) + filterRenderer: getFilterRenderer(desc, field, isShownOperators) }; + if (isShownOperators) columnProp.width = 300; + return columnProp; }); /** * Create a column from the configruation. Maps the events to call a function with the whole property @@ -266,7 +268,7 @@ export const gridUpdateToQueryUpdate = ({attribute, operator, value, type, filte index: 0 }]), filters: (oldFilterObj?.filters?.filter((filter) => attribute !== filter?.attribute) ?? []).concat(filters), - filterFields: type === 'geometry' ? oldFilterObj.filterFields : !isNil(value) + filterFields: type === 'geometry' ? oldFilterObj.filterFields : !isNil(value) || operator === 'isNull' ? upsertFilterField((oldFilterObj.filterFields || []), {attribute: attribute}, { attribute, rowId: Date.now(),