From 9d78807a0acc70585a0218c13e984b045c1ceb86 Mon Sep 17 00:00:00 2001 From: RowHeat <40065760+rowheat02@users.noreply.github.com> Date: Tue, 5 May 2026 21:26:52 +0545 Subject: [PATCH] Introduce slider varient for Dynamic filter #12268 (#12311) (cherry picked from commit c7a936555117bfa5adf59b24af33af575c727529) --- .../widgets/builder/wizard/FilterWizard.jsx | 8 +- .../wizard/filter/FilterCheckboxList.jsx | 4 +- .../builder/wizard/filter/FilterChipList.jsx | 5 +- .../components/AttributeSelector.jsx | 4 +- .../components/FilterAttributesSection.jsx | 5 +- .../FilterDataTab/hooks/useFilterData.js | 5 +- .../FilterDataTab/hooks/useLayerAttributes.js | 4 +- .../wizard/filter/FilterDataTab/index.jsx | 14 +- .../builder/wizard/filter/FilterLayoutTab.jsx | 294 ++++++++++++++++-- .../builder/wizard/filter/FilterList.jsx | 4 +- .../builder/wizard/filter/FilterSlider.jsx | 195 ++++++++++++ .../wizard/filter/FilterSwitchList.jsx | 5 +- .../filter/__tests__/FilterLayoutTab-test.jsx | 112 ++++++- .../widgets/widget/FilterWidget.jsx | 3 +- .../widgets/widget/filter-widget.less | 4 + .../widgetbuilder/FilterBuilderContent.jsx | 17 +- .../plugins/widgetbuilder/FilterView.jsx | 83 ++++- .../__tests__/FilterView-test.jsx | 115 ++++++- .../widgetbuilder/utils/filterBuilder.js | 8 +- web/client/themes/default/less/sliders.less | 47 +++ web/client/themes/default/less/wizard.less | 33 +- web/client/translations/data.de-DE.json | 17 + web/client/translations/data.en-US.json | 17 + web/client/translations/data.es-ES.json | 17 + web/client/translations/data.fr-FR.json | 17 + web/client/translations/data.it-IT.json | 17 + 26 files changed, 973 insertions(+), 81 deletions(-) create mode 100644 web/client/components/widgets/builder/wizard/filter/FilterSlider.jsx diff --git a/web/client/components/widgets/builder/wizard/FilterWizard.jsx b/web/client/components/widgets/builder/wizard/FilterWizard.jsx index 51459e41c0a..2a14013a0f9 100644 --- a/web/client/components/widgets/builder/wizard/FilterWizard.jsx +++ b/web/client/components/widgets/builder/wizard/FilterWizard.jsx @@ -57,6 +57,7 @@ const isFilterConfigValid = (editorData = {}) => { const FilterWizard = ({ filterData = {}, editorData = {}, + selectableItems = [], onChange = () => {}, onOpenLayerSelector = () => {}, openFilterEditor = () => {}, @@ -74,7 +75,8 @@ const FilterWizard = ({ onAddFilter = () => {}, onDeleteFilter = () => {}, onRenameFilter = () => {}, - onSelectionChange = () => {} + onSelectionChange = () => {}, + onSelectableItemsChange = () => {} }) => { const [activeTab, setActiveTab] = useState('data'); @@ -94,7 +96,7 @@ const FilterWizard = ({ const tabContents = { data: , - layout: , + layout: , actions: }; @@ -108,6 +110,7 @@ const FilterWizard = ({ selections={selections} getSelectionHandler={onSelectionChange} selectedFilterId={selectedFilterId} + onSelectableItemsChange={onSelectableItemsChange} />
- {items.map(({ id, label, description, disabled }) => ( + {items.map(({ id, label, description, disabled }, index) => ( handleToggle(id)} @@ -85,4 +86,3 @@ FilterCheckboxList.propTypes = { export default FilterCheckboxList; - diff --git a/web/client/components/widgets/builder/wizard/filter/FilterChipList.jsx b/web/client/components/widgets/builder/wizard/filter/FilterChipList.jsx index 980d7b49e9c..79eff894d62 100644 --- a/web/client/components/widgets/builder/wizard/filter/FilterChipList.jsx +++ b/web/client/components/widgets/builder/wizard/filter/FilterChipList.jsx @@ -70,11 +70,11 @@ const FilterChipList = ({ className="ms-filter-chip-list-items" style={listStyle} > - {items.map(({ id, label, disabled }) => { + {items.map(({ id, label, disabled }, index) => { const active = selectedValues.includes(id); return ( { - onValueAttributeChange(option?.value); + onValueAttributeChange(option); }; const handleLabelAttributeChange = (option) => { - onLabelAttributeChange(option?.value); + onLabelAttributeChange(option); }; const handleSortByAttributeChange = (option) => { @@ -153,4 +153,3 @@ FilterAttributesSection.propTypes = { }; export default FilterAttributesSection; - diff --git a/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useFilterData.js b/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useFilterData.js index ecb41bce5ba..a1440a8a4e9 100644 --- a/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useFilterData.js +++ b/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useFilterData.js @@ -80,7 +80,9 @@ export const useFilterData = (data = {}) => { // Attributes const valueAttribute = filterData.valueAttribute ?? null; + const valueAttributeType = filterData.valueAttributeType ?? null; const labelAttribute = filterData.labelAttribute ?? null; + const labelAttributeType = filterData.labelAttributeType ?? null; const sortByAttribute = filterData.sortByAttribute ?? null; const sortOrder = filterData.sortOrder; @@ -111,7 +113,9 @@ export const useFilterData = (data = {}) => { // Attributes valueAttribute, + valueAttributeType, labelAttribute, + labelAttributeType, sortByAttribute, sortOrder, @@ -128,4 +132,3 @@ export const useFilterData = (data = {}) => { }; }, [data]); }; - diff --git a/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useLayerAttributes.js b/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useLayerAttributes.js index 7d7a8f7a266..3d7d998039f 100644 --- a/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useLayerAttributes.js +++ b/web/client/components/widgets/builder/wizard/filter/FilterDataTab/hooks/useLayerAttributes.js @@ -20,7 +20,8 @@ const getLayerKey = (layer, layerId = null) => { const mapAttributesToOptions = (attributes = []) => { return attributes.map(attribute => ({ value: attribute.value || attribute.attribute || attribute.name, - label: attribute.label || attribute.alias || attribute.value || attribute.attribute || attribute.name + label: attribute.label || attribute.alias || attribute.value || attribute.attribute || attribute.name, + type: attribute.type })); }; @@ -110,4 +111,3 @@ export const useLayerAttributes = (selectedLayer, hasLayerSelection = false) => error: attributesError }; }; - diff --git a/web/client/components/widgets/builder/wizard/filter/FilterDataTab/index.jsx b/web/client/components/widgets/builder/wizard/filter/FilterDataTab/index.jsx index d0be37fddc0..89f73158b35 100644 --- a/web/client/components/widgets/builder/wizard/filter/FilterDataTab/index.jsx +++ b/web/client/components/widgets/builder/wizard/filter/FilterDataTab/index.jsx @@ -85,8 +85,17 @@ const FilterDataTab = ({ }, [onEditorChange]); // Generic handlers using the factory function - const handleValueAttributeChange = createChangeHandler('data.valueAttribute'); - const handleLabelAttributeChange = createChangeHandler('data.labelAttribute'); + const handleValueAttributeChange = useCallback((option) => { + onChange('data.valueAttribute', option?.value); + onChange('data.valueAttributeType', option?.type); + if (!filterDataState.labelAttribute) { + onChange('data.labelAttributeType', option?.type); + } + }, [onChange, filterDataState.labelAttribute]); + const handleLabelAttributeChange = useCallback((option) => { + onChange('data.labelAttribute', option?.value); + onChange('data.labelAttributeType', option?.type); + }, [onChange]); const handleSortByAttributeChange = createChangeHandler('data.sortByAttribute'); const handleSortOrderChange = createChangeHandler('data.sortOrder'); const handleMaxFeaturesChange = createChangeHandler('data.maxFeatures'); @@ -207,4 +216,3 @@ FilterDataTab.propTypes = { }; export default FilterDataTab; - diff --git a/web/client/components/widgets/builder/wizard/filter/FilterLayoutTab.jsx b/web/client/components/widgets/builder/wizard/filter/FilterLayoutTab.jsx index cacac2aac69..3a30f32dd36 100644 --- a/web/client/components/widgets/builder/wizard/filter/FilterLayoutTab.jsx +++ b/web/client/components/widgets/builder/wizard/filter/FilterLayoutTab.jsx @@ -5,8 +5,8 @@ * 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, { useState } from 'react'; -import { FormGroup, ControlLabel, InputGroup, FormControl, Panel, Glyphicon, Collapse, Checkbox } from 'react-bootstrap'; +import React, { useEffect, useRef, useState } from 'react'; +import { FormGroup, ControlLabel, InputGroup, FormControl, Panel, Glyphicon, Collapse, Checkbox, Button, OverlayTrigger, Tooltip } from 'react-bootstrap'; import Select from 'react-select'; import ColorSelector from '../../../../style/ColorSelector'; import FontAwesomeIconSelector from './FontAwesomeIconSelector/FontAwesomeIconSelector'; @@ -19,6 +19,105 @@ import InfoPopover from '../../../widget/InfoPopover'; import { USER_DEFINED_TYPES } from './FilterDataTab/constants'; const LocalizedFormControl = localizedProps('placeholder')(FormControl); +const TICK_INPUT_DEBOUNCE_TIME = 300; +const normalizeTickAngle = (value) => { + const angle = Number(value); + if (!Number.isFinite(angle)) { + return -90; + } + return Math.max(-90, Math.min(90, angle)); +}; + +const TickAngleControl = ({ + value = -90, + onChange = () => {} +}) => { + const normalizedValue = normalizeTickAngle(value); + const [inputValue, setInputValue] = useState(String(normalizedValue)); + + useEffect(() => { + setInputValue(String(normalizedValue)); + }, [normalizedValue]); + + const commitValue = (nextValue) => { + onChange(normalizeTickAngle(nextValue)); + }; + + return ( +
+ commitValue(event.target.value)} + /> + { + const nextValue = event.target.value; + setInputValue(nextValue); + if (nextValue !== '') { + commitValue(nextValue); + } + }} + style={{ width: 68, flex: '0 0 68px', textAlign: 'right' }} + /> +
+ ); +}; + +// Keep typing local and debounced layout update later. +const DebouncedLocalizedFormControl = ({ + value = '', + onChange = () => {}, + debounceTime = TICK_INPUT_DEBOUNCE_TIME, + ...props +}) => { + const inputValue = value || ''; + const [localValue, setLocalValue] = useState(inputValue); + const committedValue = useRef(inputValue); + const onChangeRef = useRef(onChange); + + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + useEffect(() => { + committedValue.current = inputValue; + setLocalValue(inputValue); + }, [inputValue]); + + useEffect(() => { + let timeout; + if (localValue !== committedValue.current) { + timeout = setTimeout(() => { + committedValue.current = localValue; + onChangeRef.current(localValue); + }, debounceTime); + } + return () => { + if (timeout) { + clearTimeout(timeout); + } + }; + }, [localValue, debounceTime]); + + return ( + setLocalValue(event.target.value)} + /> + ); +}; const SELECTION_MODE_OPTIONS = [ { value: 'multiple', label: 'Multiple', labelKey: 'widgets.filterWidget.multiple' }, @@ -34,11 +133,14 @@ const FilterLayoutTab = ({ data = {}, onChange = () => {}, onEditorChange = () => {}, - selections = {} + selections = {}, + selectableItems = [] }) => { const layout = data?.layout || {}; + const filterItems = Array.isArray(selectableItems) ? selectableItems : []; const [expandedPanel, setExpandedPanel] = useState("items"); const isStyleList = data?.data?.userDefinedType === USER_DEFINED_TYPES.STYLE_LIST; + const showTickAutofillButton = layout.variant === 'slider'; // Localized options for selection mode const selectedSelectionMode = SELECTION_MODE_OPTIONS.find(opt => opt.value === layout.selectionMode); @@ -53,11 +155,58 @@ const FilterLayoutTab = ({ DIRECTION_OPTIONS, selectedDirection ); + const isSliderVariant = layout.variant === 'slider'; + const showTicks = layout.showTicks !== false; + const tickAngle = normalizeTickAngle(layout.tickAngle); + const variantOptions = [ + { value: 'checkbox', label: 'Checkbox' }, + { value: 'button', label: 'Button' }, + { value: 'dropdown', label: 'Dropdown' }, + { value: 'switch', label: 'Switch' }, + ...(layout.selectionMode === 'single' ? [{ value: 'slider', label: 'Slider' }] : []) + ]; + const localizedSelectionModeOptionsWithDisabledMultiple = localizedSelectionModeOptions.map(opt => ( + opt.value === 'multiple' && isSliderVariant + ? { ...opt, disabled: true } + : opt + )); const handlePanelToggle = (panelName) => { setExpandedPanel(expandedPanel === panelName ? null : panelName); }; + const handleVariantChange = (val) => { + onChange('layout.variant', val?.value); + if (val?.value === 'slider') { + onChange('layout.selectionMode', 'single'); + onEditorChange('selections', { + ...selections, + [data.id]: selections?.[data.id]?.length > 0 ? [selections[data.id][0]] : [] + }); + } + }; + + const handleSelectionModeChange = (val) => { + const nextSelectionMode = val?.value; + const currentSelections = selections?.[data.id] || []; + onChange('layout.selectionMode', nextSelectionMode); + onEditorChange('selections', { + ...selections, + [data.id]: nextSelectionMode === 'single' ? (currentSelections.length > 0 ? [currentSelections[0]] : []) : currentSelections + }); + if (nextSelectionMode !== 'single' && isSliderVariant) { + onChange('layout.variant', 'checkbox'); + } + }; + + const handleAutofillTickValues = () => { + const tickValues = filterItems + .map(item => item?.id) + .filter(item => item !== undefined && item !== null && item !== '') + .join(', '); + onChange('layout.tickValues', tickValues); + }; + return (
onChange('layout.variant', val?.value)} + onChange={handleVariantChange} /> @@ -205,18 +349,10 @@ const FilterLayoutTab = ({