Skip to content

Commit 2712c3a

Browse files
authored
fix: No selection mode implementations (geosolutions-it#11990)
1 parent 5beff13 commit 2712c3a

23 files changed

Lines changed: 300 additions & 153 deletions

web/client/components/widgets/builder/wizard/FilterWizard.jsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import WizardContainer from '../../../misc/wizard/WizardContainer';
1818
import { wizardHandlers } from '../../../misc/wizard/enhancers';
1919
import WidgetOptions from './common/WidgetOptions';
2020
import Message from '../../../I18N/Message';
21-
import { areAllForceSelectionsValid } from '../../../../plugins/widgetbuilder/utils/filterBuilder';
21+
import { areAllForceSelectionsValid, areAllCustomNoSelectionFiltersValid } from '../../../../plugins/widgetbuilder/utils/filterBuilder';
22+
import { isFilterValid } from '../../../../utils/FilterUtils';
2223

2324

2425
const Wizard = wizardHandlers(WizardContainer);
@@ -87,7 +88,8 @@ const FilterWizard = ({
8788
useEffect(() => {
8889
const isConfigValid = isFilterConfigValid(editorData);
8990
const isSelectionValid = areAllForceSelectionsValid(editorData.filters, editorData.selections);
90-
setValid(isConfigValid && isSelectionValid);
91+
const isCustomFilterValid = areAllCustomNoSelectionFiltersValid(editorData.filters, isFilterValid);
92+
setValid(isConfigValid && isSelectionValid && isCustomFilterValid);
9193
}, [editorData, setValid]);
9294

9395
const tabContents = {
@@ -156,7 +158,9 @@ const FilterWizard = ({
156158
onFinish={onFinish}
157159
isStepValid={(n) =>
158160
n === 0
159-
? isFilterConfigValid(editorData) && areAllForceSelectionsValid(editorData.filters, editorData.selections)
161+
? isFilterConfigValid(editorData)
162+
&& areAllForceSelectionsValid(editorData.filters, editorData.selections)
163+
&& areAllCustomNoSelectionFiltersValid(editorData.filters, isFilterValid)
160164
: true
161165
}
162166
hideButtons

web/client/components/widgets/builder/wizard/filter/FilterDataTab/components/DefaultFilterInput.jsx

Lines changed: 0 additions & 59 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import React from 'react';
9+
import PropTypes from 'prop-types';
10+
import { FormGroup, ControlLabel, InputGroup, Glyphicon, Button, Alert } from 'react-bootstrap';
11+
import Select from 'react-select';
12+
import InfoPopover from '../../../../../widget/InfoPopover';
13+
import { FILTER_SELECTION_MODE_OPTIONS, FILTER_SELECTION_MODES } from '../constants';
14+
import { HTML, Message } from '../../../../../../I18N/I18N';
15+
import { useLocalizedOptions } from '../../hooks/useLocalizedOptions';
16+
17+
/**
18+
* No selection mode selector component (No filter / Exclude / Custom)
19+
* When Custom is selected, shows a filter button inline beside the dropdown.
20+
*/
21+
const FilterSelectionModeSelector = ({
22+
value,
23+
onChange,
24+
defaultFilter,
25+
onDefineFilter,
26+
isFilterValid
27+
}) => {
28+
const selectedOption = FILTER_SELECTION_MODE_OPTIONS.find(opt => opt.value === value);
29+
const { localizedOptions, localizedSelectedOption } = useLocalizedOptions(
30+
FILTER_SELECTION_MODE_OPTIONS,
31+
selectedOption
32+
);
33+
34+
const handleChange = (option) => {
35+
onChange(option?.value || FILTER_SELECTION_MODES.NO_FILTER);
36+
};
37+
38+
const showCustomFilterWarning = value === FILTER_SELECTION_MODES.CUSTOM
39+
&& !(defaultFilter && isFilterValid && isFilterValid(defaultFilter));
40+
41+
return (
42+
<>
43+
<FormGroup className="form-group-flex">
44+
<ControlLabel>
45+
<Message msgId="widgets.filterWidget.noSelectionMode" />&nbsp;
46+
<InfoPopover
47+
id="ms-filter-no-selection-mode-help"
48+
placement="right"
49+
trigger={['hover', 'focus']}
50+
text={
51+
<div className="ms-filter-type-help-popover">
52+
<HTML msgId="widgets.filterWidget.noSelectionModeTooltip" />
53+
</div>
54+
}
55+
/>
56+
</ControlLabel>
57+
<InputGroup>
58+
<Select
59+
value={localizedSelectedOption}
60+
options={localizedOptions}
61+
placeholder="Select mode..."
62+
onChange={handleChange}
63+
clearable={false}
64+
/>
65+
{value === FILTER_SELECTION_MODES.CUSTOM && onDefineFilter && (
66+
<InputGroup.Button>
67+
<Button
68+
bsStyle={defaultFilter && isFilterValid && isFilterValid(defaultFilter) ? 'success' : 'primary'}
69+
onClick={onDefineFilter}
70+
>
71+
<Glyphicon glyph="filter" />
72+
</Button>
73+
</InputGroup.Button>
74+
)}
75+
</InputGroup>
76+
</FormGroup>
77+
{showCustomFilterWarning && (
78+
<Alert bsStyle="warning" className="ms-filter-custom-filter-warning">
79+
<Message msgId="widgets.filterWidget.customFilterRequiredWarning" />
80+
</Alert>
81+
)}
82+
</>
83+
);
84+
};
85+
86+
FilterSelectionModeSelector.propTypes = {
87+
value: PropTypes.string,
88+
onChange: PropTypes.func.isRequired,
89+
defaultFilter: PropTypes.object,
90+
onDefineFilter: PropTypes.func,
91+
isFilterValid: PropTypes.func
92+
};
93+
94+
export default FilterSelectionModeSelector;

web/client/components/widgets/builder/wizard/filter/FilterDataTab/constants.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,14 @@ export const USER_DEFINED_TYPE_OPTIONS = [
6363
{ value: USER_DEFINED_TYPES.STYLE_LIST, label: 'Style list', labelKey: 'widgets.filterWidget.styleList' }
6464
];
6565

66+
export const FILTER_SELECTION_MODES = {
67+
NO_FILTER: 'noFilter',
68+
EXCLUDE: 'exclude',
69+
CUSTOM: 'custom'
70+
};
71+
72+
export const FILTER_SELECTION_MODE_OPTIONS = [
73+
{ value: FILTER_SELECTION_MODES.NO_FILTER, labelKey: 'widgets.filterWidget.noFilter' },
74+
{ value: FILTER_SELECTION_MODES.EXCLUDE, labelKey: 'widgets.filterWidget.exclude' },
75+
{ value: FILTER_SELECTION_MODES.CUSTOM, labelKey: 'widgets.filterWidget.custom' }
76+
];

web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useFilterData.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88
import { useMemo } from 'react';
9-
import { DATA_SOURCE_TYPES, VALUES_FROM_TYPES, USER_DEFINED_TYPES } from '../constants';
9+
import { DATA_SOURCE_TYPES, VALUES_FROM_TYPES, USER_DEFINED_TYPES, FILTER_SELECTION_MODES } from '../constants';
1010

1111
// Inline layer utility functions
1212
const normalizeLayer = (layer) => {
@@ -92,6 +92,7 @@ export const useFilterData = (data = {}) => {
9292
// User defined items
9393
const userDefinedItems = normalizeUserDefinedItems(filterData.userDefinedItems);
9494
const defaultFilter = filterData.defaultFilter;
95+
const noSelectionMode = filterData.noSelectionMode ?? FILTER_SELECTION_MODES.NO_FILTER;
9596

9697
return {
9798
// Raw data
@@ -120,6 +121,7 @@ export const useFilterData = (data = {}) => {
120121
userDefinedType,
121122
userDefinedItems,
122123
defaultFilter,
124+
noSelectionMode,
123125

124126
// Flags
125127
hasLayerSelection: !!selectedLayerObject

web/client/components/widgets/builder/wizard/filter/FilterDataTab/index.jsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import ValuesFromSelector from './components/ValuesFromSelector';
1818
import FilterAttributesSection from './components/FilterAttributesSection';
1919
import MaxFeaturesInput from './components/MaxFeaturesInput';
2020
import FilterCompositionSelector from './components/FilterCompositionSelector';
21-
import DefaultFilterInput from './components/DefaultFilterInput';
21+
import FilterSelectionModeSelector from './components/FilterSelectionModeSelector';
2222
import { VALUES_FROM_TYPES, USER_DEFINED_TYPES } from './constants';
23+
import { isFilterValid } from '../../../../../../utils/FilterUtils';
2324

2425
const FilterDataTab = ({
2526
data = {},
@@ -71,9 +72,8 @@ const FilterDataTab = ({
7172
};
7273

7374
const handleEditUserDefinedItemFilter = useCallback((itemId) => {
74-
// Store which user-defined item is being edited
75+
onEditorChange('editingDefaultFilter', false);
7576
onEditorChange('editingUserDefinedItemId', itemId);
76-
// Small delay to ensure state is updated before opening filter editor
7777
setTimeout(() => {
7878
openFilterEditor();
7979
}, 0);
@@ -91,6 +91,7 @@ const FilterDataTab = ({
9191
const handleSortOrderChange = createChangeHandler('data.sortOrder');
9292
const handleMaxFeaturesChange = createChangeHandler('data.maxFeatures');
9393
const handleFilterCompositionChange = createChangeHandler('data.filterComposition');
94+
const handleNoSelectionModeChange = createChangeHandler('data.noSelectionMode');
9495
const handleUserDefinedTypeChange = useCallback((value) => {
9596
onChange('data.userDefinedType', value);
9697
// Clear userDefinedItems when type changes
@@ -102,9 +103,8 @@ const FilterDataTab = ({
102103
}, [onChange]);
103104

104105
const handleEditDefaultFilter = useCallback(() => {
105-
// Store flag indicating we're editing the defaultFilter
106+
onEditorChange('editingUserDefinedItemId', null);
106107
onEditorChange('editingDefaultFilter', true);
107-
// Small delay to ensure state is updated before opening filter editor
108108
setTimeout(() => {
109109
openFilterEditor();
110110
}, 0);
@@ -184,9 +184,12 @@ const FilterDataTab = ({
184184
onChange={handleFilterCompositionChange}
185185
/>
186186

187-
<DefaultFilterInput
187+
<FilterSelectionModeSelector
188+
value={filterDataState.noSelectionMode}
189+
onChange={handleNoSelectionModeChange}
188190
defaultFilter={filterDataState?.defaultFilter}
189191
onDefineFilter={handleEditDefaultFilter}
192+
isFilterValid={isFilterValid}
190193
/>
191194
</>
192195
)}

web/client/components/widgets/builder/wizard/filter/__tests__/DefaultFilterInput-test.jsx

Lines changed: 0 additions & 46 deletions
This file was deleted.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2025, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
import expect from 'expect';
9+
import React from 'react';
10+
import ReactDOM from 'react-dom';
11+
import FilterSelectionModeSelector from '../FilterDataTab/components/FilterSelectionModeSelector';
12+
import { FILTER_SELECTION_MODES } from '../FilterDataTab/constants';
13+
14+
describe('FilterSelectionModeSelector', () => {
15+
beforeEach((done) => {
16+
document.body.innerHTML = '<div id="container"></div>';
17+
setTimeout(done);
18+
});
19+
20+
afterEach((done) => {
21+
ReactDOM.unmountComponentAtNode(document.getElementById('container'));
22+
document.body.innerHTML = '';
23+
setTimeout(done);
24+
});
25+
26+
it('shows warning when Custom is selected and defaultFilter is invalid or missing', () => {
27+
const container = document.getElementById('container');
28+
ReactDOM.render(
29+
<FilterSelectionModeSelector
30+
value={FILTER_SELECTION_MODES.CUSTOM}
31+
onChange={() => {}}
32+
onDefineFilter={() => {}}
33+
isFilterValid={() => false}
34+
/>,
35+
container
36+
);
37+
38+
const warning = container.querySelector('.ms-filter-custom-filter-warning');
39+
expect(warning).toExist();
40+
expect(warning.className).toContain('alert-warning');
41+
42+
const filterIcon = container.querySelector('.glyphicon-filter');
43+
expect(filterIcon).toExist();
44+
});
45+
});

web/client/epics/dashboard.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,13 @@ export const handleDashboardWidgetsFilterPanel = (action$, {getState = () => {}}
101101

102102
if (action.filterObj) {
103103
actions.push(onEditorChange(getWidgetFilterKey(currentState), action.filterObj));
104-
// Reset editingUserDefinedItemId after saving filter
105-
if (editingWidget.editingUserDefinedItemId) {
106-
actions.push(onEditorChange('editingUserDefinedItemId', null));
107-
}
108-
// Reset editingDefaultFilter after saving filter
109-
if (editingWidget.editingDefaultFilter) {
110-
actions.push(onEditorChange('editingDefaultFilter', false));
111-
}
104+
}
105+
// Reset edit context when query panel closes (save or cancel)
106+
if (editingWidget.editingUserDefinedItemId) {
107+
actions.push(onEditorChange('editingUserDefinedItemId', null));
108+
}
109+
if (editingWidget.editingDefaultFilter) {
110+
actions.push(onEditorChange('editingDefaultFilter', false));
112111
}
113112

114113
return Rx.Observable.merge(

0 commit comments

Comments
 (0)