Skip to content

Commit

Permalink
geosolutions-it#9706: Allow time range filtering for Attribute Table …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
mahmoudadel54 committed Nov 23, 2023
1 parent 7f3c2e7 commit fc0f4c8
Show file tree
Hide file tree
Showing 27 changed files with 594 additions and 249 deletions.
8 changes: 0 additions & 8 deletions web/client/actions/featuregrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ 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';
Expand Down Expand Up @@ -235,13 +234,6 @@ 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,
Expand Down
4 changes: 2 additions & 2 deletions web/client/components/data/featuregrid/enhancers/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const featuresToGrid = compose(
editors,
dataStreamFactory,
virtualScroll: true,
isShownOperators: false
isWithinAttrTbl: false
}),
withPropsOnChange("showDragHandle", ({showDragHandle = true} = {}) => ({
className: showDragHandle ? 'feature-grid-drag-handle-show' : 'feature-grid-drag-handle-hide'
Expand Down Expand Up @@ -172,7 +172,7 @@ const featuresToGrid = compose(
},
getFilterRenderer: getFilterRendererFunc,
getFormatter: (desc) => getFormatter(desc, (props.fields ?? []).find(f => f.name === desc.name), {dateFormats: props.dateFormats}),
isShownOperators: props.isShownOperators
isWithinAttrTbl: props.isWithinAttrTbl
}))
});
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class AttributeFilter extends React.PureComponent {
tooltipMsgId: PropTypes.string,
operator: PropTypes.string,
type: PropTypes.string,
isShownOperators: PropTypes.bool
isWithinAttrTbl: PropTypes.bool
};

static contextTypes = {
Expand All @@ -39,7 +39,7 @@ class AttributeFilter extends React.PureComponent {
column: {},
placeholderMsgId: "featuregrid.filter.placeholders.default",
operator: "=",
isShownOperators: false
isWithinAttrTbl: false
};
constructor(props) {
super(props);
Expand All @@ -50,7 +50,7 @@ class AttributeFilter extends React.PureComponent {
booleanOperators: ["="],
defaultOperators: ["=", ">", "<", ">=", "<=", "<>", "isNull"],
timeDateOperators: ["=", ">", "<", ">=", "<=", "<>", "><", "isNull"],
operator: this.props.operator || "="
operator: this.props.isWithinAttrTbl ? "=" : ""
};
}
getOperator = (type) => {
Expand Down Expand Up @@ -83,14 +83,17 @@ class AttributeFilter extends React.PureComponent {
}
const placeholder = getMessageById(this.context.messages, this.props.placeholderMsgId) || "Search";
let inputKey = 'header-filter-' + this.props.column.key;
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 (<div className="rw-widget">
<input
disabled={this.props.disabled || this.state.operator === 'isNull'}
disabled={this.props.disabled || isNullOperator}
key={inputKey}
type="text"
className="form-control input-sm"
placeholder={placeholder}
value={this.state?.value ?? this.props.value}
value={isValueExist}
onChange={this.handleChange}/>
</div>);
}
Expand All @@ -111,21 +114,23 @@ class AttributeFilter extends React.PureComponent {
fieldName="operator"
fieldRowId={1}
onSelect={(selectedOperator)=>{
this.setState({ operator: selectedOperator, value: selectedOperator === 'isNull' ? undefined : this.state?.value ?? this.props.value });
let isNullOperatorSelected = selectedOperator === 'isNull';
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 isNullOperatorSelected = selectedOperator === 'isNull';
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});
if (isValueExist || isNullOperatorSelected || isOperatorChangedFromIsNullAndValueNotExist ) this.props.onChange({value: isNullOperatorSelected ? null : isValueExist, attribute: this.props.column && this.props.column.key, inputOperator: selectedOperator});
}}
fieldValue={this.state.operator}
onUpdateField={this.updateFieldElement}/>
onUpdateField={() => {}}/>
);
};
render() {
let inputKey = 'header-filter--' + this.props.column.key;
return (
<div key={inputKey} className={`form-group${(this.props.valid ? "" : " has-error")}`}>
{this.props.isShownOperators ? this.renderOperatorField() : null}
{this.props.isWithinAttrTbl ? this.renderOperatorField() : null}
{this.renderTooltip(this.renderInput())}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class DateFilter extends AttributeFilter {
if (operator === '><') {
return (
<UTCDateTimePickerWithRange
isWithinAttrTbl={this.props.isWithinAttrTbl}
key={inputKey}
disabled={this.props.disabled}
format={format}
Expand All @@ -79,11 +80,12 @@ class DateFilter extends AttributeFilter {
type={this.props.type}
time={this.props.type === 'time'}
calendar={this.props.type === 'date-time' || this.props.type === 'date'}
onChange={(date, stringDate) => this.handleChange(date, stringDate)}
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 @@ -100,6 +102,21 @@ class DateFilter extends AttributeFilter {
handleChange = (value, stringValue) => {
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 });
}
}

export default getContext({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,32 @@ export default compose(
}),
withHandlers({
onChange: props => ({ value, attribute, stringValue, inputOperator } = {}) => {
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 = "=";
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 {
props.onValueChange(value);
props.onChange({
value: { startDate: value?.startDate, endDate: value?.endDate, inputOperator },
operator: inputOperator,
type: props.type,
attribute
});
}
props.onValueChange(value);
props.onChange({
value: { startDate: value, operator },
operator: inputOperator || enhancedOperator,
type: props.type,
attribute
});
}
}),
defaultProps({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default compose(
props.onChange({
rawValue: value,
value: trim(value) ? trim(value) : undefined,
operator: inputOperator || "ilike", // need to read operator from redux beased on operator selected option
operator: inputOperator || "ilike",
type: 'string',
attribute
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(<AttributeFilter isWithinAttrTbl={"true"} value={"TEST"}/>, 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(<AttributeFilter isWithinAttrTbl={false} value={"TEST"}/>, 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);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,24 @@ describe('Test for BaseDateTimeFilter component', () => {
expect(el).toExist();

});
it('render with range operator ><', () => {
// for type date
ReactDOM.render(<BaseDateTimeFilter type="date" value={{ operator: '><'}} 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(<BaseDateTimeFilter type="time" value={{ operator: '><'}} 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(<BaseDateTimeFilter type="date-time" value={{ operator: '><'}} 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();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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(<Cmp />, 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(<Cmp />, 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(<Cmp />, 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(<Cmp />, 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(<Cmp />, 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(<Cmp />, 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(<Cmp />, document.getElementById("container"));
operatorDropdownEl = document.getElementsByClassName('rw-dropdownlist')[0];
expect(operatorDropdownEl).toExist();
});
});
Loading

0 comments on commit fc0f4c8

Please sign in to comment.