diff --git a/web/client/components/widgets/builder/wizard/FilterWizard.jsx b/web/client/components/widgets/builder/wizard/FilterWizard.jsx index 2a14013a0f9..62d5138e4f2 100644 --- a/web/client/components/widgets/builder/wizard/FilterWizard.jsx +++ b/web/client/components/widgets/builder/wizard/FilterWizard.jsx @@ -95,8 +95,8 @@ const FilterWizard = ({ }, [editorData, setValid]); const tabContents = { - data: , - layout: , + data: , + layout: , actions: }; diff --git a/web/client/components/widgets/builder/wizard/common/interactions/InteractionButtons.jsx b/web/client/components/widgets/builder/wizard/common/interactions/InteractionButtons.jsx index b5a45ff285c..eff3789b82c 100644 --- a/web/client/components/widgets/builder/wizard/common/interactions/InteractionButtons.jsx +++ b/web/client/components/widgets/builder/wizard/common/interactions/InteractionButtons.jsx @@ -24,38 +24,27 @@ const TButton = tooltip(Button); * @param {function} setShowConfiguration toggles the UI for configuration * @param {boolean} isPluggable tells if the interaction can be plugged or not * @param {boolean} isConfigurable tells if the interaction can be configured or not + * @param {object} plugConstraints describes whether plugging is disabled and why * @returns {React.ReactElement} */ -const InteractionButtons = ({ plugged, setPlugged, showConfiguration, setShowConfiguration = () => {}, isPluggable, isConfigurable, notConnectableForSpecialCase, notConnectableForSpecialCaseMsg}) => { - - if (notConnectableForSpecialCase) { - return ( - - } - className="square-button" - > - - - - ); - } +const InteractionButtons = ({ plugged, setPlugged, showConfiguration, setShowConfiguration = () => {}, isPluggable, isConfigurable, plugConstraints = {} }) => { + const { disabled: isPlugConstrained = false, reason: plugConstraintReason = null } = plugConstraints; return ( - + {isConfigurable && setShowConfiguration(!showConfiguration)} borderTransparent - tooltip={} + tooltip={plugConstraintReason || } variant={showConfiguration ? "primary" : undefined} > } setPlugged(!plugged)} borderTransparent variant={plugged ? "success" : undefined} @@ -67,4 +56,3 @@ const InteractionButtons = ({ plugged, setPlugged, showConfiguration, setShowCon }; export default InteractionButtons; - diff --git a/web/client/components/widgets/builder/wizard/common/interactions/InteractionEventsSelector.jsx b/web/client/components/widgets/builder/wizard/common/interactions/InteractionEventsSelector.jsx index 437b4f975fb..642558bf649 100644 --- a/web/client/components/widgets/builder/wizard/common/interactions/InteractionEventsSelector.jsx +++ b/web/client/components/widgets/builder/wizard/common/interactions/InteractionEventsSelector.jsx @@ -14,25 +14,27 @@ import { Glyphicon } from 'react-bootstrap'; import { getWidgetInteractionTreeGenerated, getEditingWidget, getAllInteractionsWhileEditingSelector } from '../../../../../../selectors/widgets'; import InteractionTargetsList from './InteractionTargetsList'; import './interaction-wizard.less'; -import { detachSingleChildCollections, filterTreeWithTarget } from '../../../../../../utils/InteractionUtils'; +import { detachSingleChildCollections, filterTreeWithTarget, filterDimensionTreeByValueAttributeType } from '../../../../../../utils/InteractionUtils'; import Message from '../../../../../I18N/Message'; +import tooltip from '../../../../../misc/enhancers/tooltip'; -const targetTitleTranslationMap = { - "Apply filter": "widgets.filterWidget.applyFilter", - "Apply style": "widgets.filterWidget.applyStyle" -}; - +const TButton = tooltip(Button); -const InteractionEventsSelector = ({target, expanded, toggleExpanded = () => {}, interactionTree, interactions, sourceWidgetId, currentSourceId, onEditorChange, alreadyExistingInteractions}) => { +export const InteractionEventsSelector = ({target, expanded, toggleExpanded = () => {}, interactionTree, interactions, sourceWidgetId, currentSourceId, onEditorChange, alreadyExistingInteractions, valueAttributeType, sourceSelectionMode, targetTitleMsgIds = {}, removable = false, onRemove = () => {}}) => { const filteredInteractionTree = useMemo(() => { - const filteredTree = filterTreeWithTarget(interactionTree, target) || []; - return detachSingleChildCollections(filteredTree, ['widgets', 'traces', "layers"]); - }, [interactionTree]); + const filteredTree = filterTreeWithTarget(interactionTree, target) || { children: [] }; + const dimensionFilteredTree = target?.targetType === 'applyDimension' + ? filterDimensionTreeByValueAttributeType(filteredTree, valueAttributeType) + : filteredTree; + return detachSingleChildCollections(dimensionFilteredTree, ['widgets', 'traces', "map", "layers"]) || { children: [] }; + }, [interactionTree, target, valueAttributeType]); + const hasConnectableNodes = (filteredInteractionTree?.children || []).length > 0; + const targetTitleMsgId = targetTitleMsgIds[target.targetType]; return ( - + - - - + + {targetTitleMsgId ? : target.title} + + {removable && ( + } + style={{ padding: 0, background: 'transparent' }}> + + + )} {expanded && - + {hasConnectableNodes ? ( + + ) : ( + + + + )} } ); diff --git a/web/client/components/widgets/builder/wizard/common/interactions/InteractionTargetsList.jsx b/web/client/components/widgets/builder/wizard/common/interactions/InteractionTargetsList.jsx index 2ee57d4689e..220a51b42b1 100644 --- a/web/client/components/widgets/builder/wizard/common/interactions/InteractionTargetsList.jsx +++ b/web/client/components/widgets/builder/wizard/common/interactions/InteractionTargetsList.jsx @@ -10,7 +10,7 @@ import FlexBox from '../../../../../layout/FlexBox'; // import { filterTreeWithTarget } from '../../../../../../utils/InteractionUtils'; import InteractionsRow from './InteractionsRow'; -const InteractionTargetsList = ({target, interactionTree, interactions, sourceWidgetId, currentSourceId, onEditorChange, filteredInteractionTree, alreadyExistingInteractions}) => { +const InteractionTargetsList = ({target, interactionTree, interactions, sourceWidgetId, currentSourceId, onEditorChange, filteredInteractionTree, alreadyExistingInteractions, sourceSelectionMode}) => { const renderContainer = (children) => ( @@ -26,6 +26,7 @@ const InteractionTargetsList = ({target, interactionTree, interactions, sourceWi currentSourceId={currentSourceId} onEditorChange={onEditorChange} alreadyExistingInteractions={alreadyExistingInteractions} + sourceSelectionMode={sourceSelectionMode} /> ))} @@ -39,4 +40,3 @@ const InteractionTargetsList = ({target, interactionTree, interactions, sourceWi }; export default InteractionTargetsList; - diff --git a/web/client/components/widgets/builder/wizard/common/interactions/InteractionsEditor.jsx b/web/client/components/widgets/builder/wizard/common/interactions/InteractionsEditor.jsx index 2f104d6954d..cc3cce91684 100644 --- a/web/client/components/widgets/builder/wizard/common/interactions/InteractionsEditor.jsx +++ b/web/client/components/widgets/builder/wizard/common/interactions/InteractionsEditor.jsx @@ -1,9 +1,21 @@ import React, { useEffect, useState } from 'react'; import InteractionEventsSelector from "./InteractionEventsSelector"; import { TARGET_TYPES } from '../../../../../../utils/InteractionUtils'; +import FlexBox from '../../../../../layout/FlexBox'; // currentSourceId is the source that will be source of the target, in filter widget 'filterId' is expected -const InteractionEditor = ({targets = [], sourceWidgetId, currentSourceId, onEditorChange = () => {}, isStyleOnly = false}) => { +const InteractionEditor = ({ + targets = [], + sourceWidgetId, + currentSourceId, + onEditorChange = () => {}, + isStyleOnly = false, + valueAttributeType, + sourceSelectionMode, + targetTitleMsgIds = {}, + removableTargetTypes = [], + onRemoveTarget = () => {} +}) => { const initialExpandedItems = targets.length > 0 ? [targets[0].targetType] : []; const [expandedItems, setExpandedItems] = useState(initialExpandedItems); const toggleExpanded = (name) => { @@ -21,7 +33,17 @@ const InteractionEditor = ({targets = [], sourceWidgetId, currentSourceId, onEdi } }, [isStyleOnly]); - return <> + useEffect(() => { + if (targets.some(target => target.targetType === TARGET_TYPES.APPLY_DIMENSION)) { + setExpandedItems(items => + items.includes(TARGET_TYPES.APPLY_DIMENSION) + ? items + : [...items, TARGET_TYPES.APPLY_DIMENSION] + ); + } + }, [targets]); + + return ( {targets.map(e => { const expanded = expandedItems.includes(e.targetType); return ( onRemoveTarget(e.targetType)} />); })} - ; + ); }; export default InteractionEditor; diff --git a/web/client/components/widgets/builder/wizard/common/interactions/InteractionsRow.jsx b/web/client/components/widgets/builder/wizard/common/interactions/InteractionsRow.jsx index 0225f08eaed..64e8d5a6258 100644 --- a/web/client/components/widgets/builder/wizard/common/interactions/InteractionsRow.jsx +++ b/web/client/components/widgets/builder/wizard/common/interactions/InteractionsRow.jsx @@ -13,6 +13,7 @@ import { Glyphicon } from 'react-bootstrap'; import { findNodeById, getItemPluggableStatus, + isLayerDimensionTarget, TARGET_TYPES } from '../../../../../../utils/InteractionUtils'; import InteractionButtons from './InteractionButtons'; @@ -20,6 +21,7 @@ import InteractionConfiguration from './InteractionConfiguration'; import { buildInteractionObject, matchesInteraction } from './interactionHelpers'; import { DEFAULT_CONFIGURATION } from './interactionConstants'; import Message from '../../../../../I18N/Message'; +import tooltip from '../../../../../misc/enhancers/tooltip'; import LocalizedString from '../../../../../I18N/LocalizedString'; @@ -28,8 +30,11 @@ const itemTitleTranslationMap = { "Map": "widgets.filterWidget.map" }; +const isMapTimeTargetNodePath = (nodePath) => nodePath === 'map.time' || /(?:^|\.)maps\[[^\]]+\]\.time$/.test(nodePath); +const TFlexBox = tooltip(FlexBox); -const InteractionsRow = ({item, target, interactions, sourceWidgetId, interactionTree, currentSourceId, onEditorChange, alreadyExistingInteractions}) => { + +const InteractionsRow = ({item, target, interactions, sourceWidgetId, interactionTree, currentSourceId, onEditorChange, alreadyExistingInteractions, sourceSelectionMode}) => { // from interactions we can derive if the target is plugged or not, and its configuration const hasChildren = item?.children?.length > 0; @@ -57,6 +62,14 @@ const InteractionsRow = ({item, target, interactions, sourceWidgetId, interactio // Get configuration directly from existing interaction or use default const configuration = existingInteraction?.configuration || DEFAULT_CONFIGURATION; const plugged = existingInteraction?.plugged || false; + const sourcePluggedConnections = alreadyExistingInteractions.filter(i => + i?.plugged === true + && i?.source?.nodePath === sourceNodePath + ); + const hasMapTimeConnection = sourcePluggedConnections.some(i => isMapTimeTargetNodePath(i?.target?.nodePath)); + const hasNonMapTimeConnection = sourcePluggedConnections.some(i => !isMapTimeTargetNodePath(i?.target?.nodePath)); + const currentItemIsMapTime = isMapTimeTargetNodePath(targetNodePath); + const mapTimeLockConflict = (hasMapTimeConnection && !currentItemIsMapTime) || (hasNonMapTimeConnection && currentItemIsMapTime); const { directlyPluggable, configuredToForcePlug } = getItemPluggableStatus(item, target, configuration); @@ -110,9 +123,73 @@ const InteractionsRow = ({item, target, interactions, sourceWidgetId, interactio return alreadyExistingInteractions.filter(i => i.source.nodePath !== sourceNodePath).some(i => (i.targetType === TARGET_TYPES.APPLY_STYLE && target.targetType === TARGET_TYPES.APPLY_STYLE) && i.target.nodePath === targetNodePath && i.plugged); }, [alreadyExistingInteractions, targetNodePath, sourceNodePath]); + const layerDimensionAlreadyConnected = useMemo(() => { + if (target.targetType !== TARGET_TYPES.APPLY_DIMENSION || !isLayerDimensionTarget(targetNodePath)) { + return false; + } + return alreadyExistingInteractions + .filter(i => i.source.nodePath !== sourceNodePath) + .some(i => + i.targetType === TARGET_TYPES.APPLY_DIMENSION + && i.target.nodePath === targetNodePath + && i.plugged + ); + }, [alreadyExistingInteractions, target.targetType, targetNodePath, sourceNodePath]); + + const dimensionMultipleSelectionDisabled = target.targetType === TARGET_TYPES.APPLY_DIMENSION + && sourceSelectionMode === 'multiple' + && !plugged; + + const plugConstraints = useMemo(() => { + if (styleAlreadyConnected) { + return { + disabled: true, + reason: + }; + } + + if (layerDimensionAlreadyConnected) { + return { + disabled: true, + reason: + }; + } + + if (dimensionMultipleSelectionDisabled) { + return { + disabled: true, + reason: + }; + } + + if (mapTimeLockConflict) { + return { + disabled: true, + reason: + }; + } + + return { + disabled: false, + reason: null + }; + }, [styleAlreadyConnected, layerDimensionAlreadyConnected, dimensionMultipleSelectionDisabled, mapTimeLockConflict]); + const rowPlugConstraints = item.type === 'element' + ? plugConstraints + : { + disabled: false, + reason: null + }; + return ( - + {hasChildren && (