diff --git a/src/components/geocoder-panel.js b/src/components/geocoder-panel.js index 1bc4e9e8e5..52c0c7f486 100644 --- a/src/components/geocoder-panel.js +++ b/src/components/geocoder-panel.js @@ -97,6 +97,16 @@ function isValid(key) { return /pk\..*\..*/.test(key); } +export function getUpdateVisDataPayload(lat, lon, text) { + return [ + [generateGeocoderDataset(lat, lon, text)], + { + keepExistingConfig: true + }, + PARSED_CONFIG + ]; +} + export default function GeocoderPanelFactory() { class GeocoderPanel extends Component { static propTypes = { @@ -122,13 +132,7 @@ export default function GeocoderPanelFactory() { bbox } = geoItem; this.removeGeocoderDataset(); - this.props.updateVisData( - [generateGeocoderDataset(lat, lon, text)], - { - keepExistingConfig: true - }, - PARSED_CONFIG - ); + this.props.updateVisData(...getUpdateVisDataPayload(lat, lon, text)); const bounds = bbox || [ lon - GEOCODER_GEO_OFFSET, lat - GEOCODER_GEO_OFFSET, diff --git a/src/components/kepler-gl.js b/src/components/kepler-gl.js index 51b25586f9..9fad47660e 100644 --- a/src/components/kepler-gl.js +++ b/src/components/kepler-gl.js @@ -39,7 +39,8 @@ import { KEPLER_GL_NAME, KEPLER_GL_VERSION, THEME, - DEFAULT_MAPBOX_API_URL + DEFAULT_MAPBOX_API_URL, + GEOCODER_DATASET_NAME } from 'constants/default-settings'; import {MISSING_MAPBOX_TOKEN} from 'constants/user-feedbacks'; @@ -52,7 +53,7 @@ import PlotContainerFactory from './plot-container'; import NotificationPanelFactory from './notification-panel'; import GeoCoderPanelFactory from './geocoder-panel'; -import {generateHashId} from 'utils/utils'; +import {filterObjectByPredicate, generateHashId} from 'utils/utils'; import {validateToken} from 'utils/mapbox-utils'; import {mergeMessages} from 'utils/locale-utils'; @@ -138,30 +139,40 @@ export const mapFieldsSelector = props => ({ locale: props.uiState.locale }); -export const sidePanelSelector = (props, availableProviders) => ({ - appName: props.appName, - version: props.version, - appWebsite: props.appWebsite, - mapStyle: props.mapStyle, - onSaveMap: props.onSaveMap, - uiState: props.uiState, - mapStyleActions: props.mapStyleActions, - visStateActions: props.visStateActions, - uiStateActions: props.uiStateActions, +export function getVisibleDatasets(datasets) { + // We don't want Geocoder dataset to be present in SidePanel dataset list + return filterObjectByPredicate(datasets, (key, value) => key !== GEOCODER_DATASET_NAME); +} - datasets: props.visState.datasets, - filters: props.visState.filters, - layers: props.visState.layers, - layerOrder: props.visState.layerOrder, - layerClasses: props.visState.layerClasses, - interactionConfig: props.visState.interactionConfig, - mapInfo: props.visState.mapInfo, - layerBlending: props.visState.layerBlending, +export const sidePanelSelector = (props, availableProviders) => { + // visibleDatasets + const filteredDatasets = getVisibleDatasets(props.visState.datasets); - width: props.sidePanelWidth, - availableProviders, - mapSaved: props.providerState.mapSaved -}); + return { + appName: props.appName, + version: props.version, + appWebsite: props.appWebsite, + mapStyle: props.mapStyle, + onSaveMap: props.onSaveMap, + uiState: props.uiState, + mapStyleActions: props.mapStyleActions, + visStateActions: props.visStateActions, + uiStateActions: props.uiStateActions, + + datasets: filteredDatasets, + filters: props.visState.filters, + layers: props.visState.layers, + layerOrder: props.visState.layerOrder, + layerClasses: props.visState.layerClasses, + interactionConfig: props.visState.interactionConfig, + mapInfo: props.visState.mapInfo, + layerBlending: props.visState.layerBlending, + + width: props.sidePanelWidth, + availableProviders, + mapSaved: props.providerState.mapSaved + }; +}; export const plotContainerSelector = props => ({ width: props.width, diff --git a/src/components/side-panel/interaction-panel/tooltip-config.js b/src/components/side-panel/interaction-panel/tooltip-config.js index a674dfca40..dd20a077c1 100644 --- a/src/components/side-panel/interaction-panel/tooltip-config.js +++ b/src/components/side-panel/interaction-panel/tooltip-config.js @@ -35,6 +35,7 @@ import Switch from 'components/common/switch'; import ItemSelector from 'components/common/item-selector/item-selector'; import {COMPARE_TYPES} from 'constants/tooltip'; import FieldSelectorFactory from '../../common/field-selector'; +import {GEOCODER_DATASET_NAME} from 'constants/default-settings'; const TooltipConfigWrapper = styled.div` .item-selector > div > div { @@ -131,14 +132,16 @@ function TooltipConfigFactory(DatasetTag, FieldSelector) { const TooltipConfig = ({config, datasets, onChange, intl}) => { return ( - {Object.keys(config.fieldsToShow).map(dataId => ( - - ))} + {Object.keys(config.fieldsToShow).map(dataId => + dataId === GEOCODER_DATASET_NAME ? null : ( + + ) + )} (predicate(entry[0], entry[1]) ? {...acc, [entry[0]]: entry[1]} : acc), + {} + ); +} diff --git a/test/browser/components/kepler-gl-test.js b/test/browser/components/kepler-gl-test.js index 0abee0a363..e03886a002 100644 --- a/test/browser/components/kepler-gl-test.js +++ b/test/browser/components/kepler-gl-test.js @@ -21,6 +21,7 @@ import React from 'react'; import test from 'tape'; import {mount} from 'enzyme'; +import sinon from 'sinon'; import {drainTasksForTesting, succeedTaskWithValues} from 'react-palm/tasks'; import configureStore from 'redux-mock-store'; import {Provider} from 'react-redux'; @@ -40,6 +41,9 @@ import { import NotificationPanelFactory from 'components/notification-panel'; import {ActionTypes} from 'actions'; import {DEFAULT_MAP_STYLES, EXPORT_IMAGE_ID} from 'constants'; +import {GEOCODER_DATASET_NAME} from 'constants/default-settings'; +// mock state +import {StateWithGeocoderDataset} from 'test/helpers/mock-state'; const KeplerGl = appInjector.get(KeplerGlFactory); const SidePanel = appInjector.get(SidePanelFactory); @@ -474,3 +478,98 @@ test('Components -> KeplerGl -> Mount -> Load custom map style task', t => { t.end(); }); + +// Test data has only the 'geocoder_dataset' dataset +// This function will return its name if it finds the dataset +// in other case it will return null +function findGeocoderDatasetName(wrapper) { + const datasetTitleContainer = wrapper.find('.dataset-name'); + let result; + try { + result = datasetTitleContainer.text(); + } catch (e) { + result = null; + } + return result; +} + +test('Components -> KeplerGl -> SidePanel -> Geocoder dataset display', t => { + drainTasksForTesting(); + + const toggleSidePanel = sinon.spy(); + + // Create custom SidePanel that will accept toggleSidePanel as a spy + function CustomSidePanelFactory(...deps) { + const OriginalSidePanel = SidePanelFactory(...deps); + const CustomSidePanel = props => { + const customUIStateActions = { + ...props.uiStateActions, + toggleSidePanel + }; + return ; + }; + return CustomSidePanel; + } + CustomSidePanelFactory.deps = SidePanelFactory.deps; + + const CustomKeplerGl = appInjector + .provide(SidePanelFactory, CustomSidePanelFactory) + .get(KeplerGlFactory); + + // Create initial state based on mocked state with geocoder dataset and use that for mocking the store + const store = mockStore({ + keplerGl: { + map: StateWithGeocoderDataset + } + }); + + let wrapper; + + t.doesNotThrow(() => { + wrapper = mount( + + state.keplerGl.map} + dispatch={store.dispatch} + /> + + ); + }, 'Should not throw error when mount KeplerGl'); + + // Check if we have 4 sidepanel tabs + t.equal(wrapper.find('.side-panel__tab').length, 4, 'should render 4 panel tabs'); + + // click layer tab + const layerTab = wrapper.find('.side-panel__tab').at(0); + layerTab.simulate('click'); + t.ok(toggleSidePanel.calledWith('layer'), 'should call toggleSidePanel with layer'); + t.notEqual( + findGeocoderDatasetName(wrapper), + GEOCODER_DATASET_NAME, + `should not be equal to ${GEOCODER_DATASET_NAME}` + ); + + // click filters tab + const filterTab = wrapper.find('.side-panel__tab').at(1); + filterTab.simulate('click'); + t.ok(toggleSidePanel.calledWith('filter'), 'should call toggleSidePanel with filter'); + t.notEqual( + findGeocoderDatasetName(wrapper), + GEOCODER_DATASET_NAME, + `should not be equal to ${GEOCODER_DATASET_NAME}` + ); + + // click interaction tab + const interactionTab = wrapper.find('.side-panel__tab').at(2); + interactionTab.simulate('click'); + t.ok(toggleSidePanel.calledWith('interaction'), 'should call toggleSidePanel with interaction'); + t.notEqual( + findGeocoderDatasetName(wrapper), + GEOCODER_DATASET_NAME, + `should not be equal to ${GEOCODER_DATASET_NAME}` + ); + + t.end(); +}); diff --git a/test/browser/components/tooltip-config-test.js b/test/browser/components/tooltip-config-test.js index d75dd7a3e9..4233246cd7 100644 --- a/test/browser/components/tooltip-config-test.js +++ b/test/browser/components/tooltip-config-test.js @@ -33,7 +33,7 @@ import DropdownList from 'components/common/item-selector/dropdown-list'; import Typeahead from 'components/common/item-selector/typeahead'; import {Hash, Delete} from 'components/common/icons'; -import {StateWFiles} from 'test/helpers/mock-state'; +import {StateWFiles, StateWithGeocoderDataset} from 'test/helpers/mock-state'; import {appInjector} from 'components/container'; const TooltipConfig = appInjector.get(TooltipConfigFactory); @@ -252,3 +252,29 @@ test('TooltipConfig - render -> tooltip format', t => { t.deepEqual(onChange.args[0], [expectedArgs0], 'should call onchange to set format'); t.end(); }); + +test('TooltipConfig -> render -> do not display Geocoder dataset fields', t => { + // Contains only a single dataset which is the geocoder_dataset + const datasets = StateWithGeocoderDataset.visState.datasets; + const tooltipConfig = StateWithGeocoderDataset.visState.interactionConfig.tooltip.config; + + const FieldSelector = appInjector.get(FieldSelectorFactory); + const onChange = sinon.spy(); + let wrapper; + + t.doesNotThrow(() => { + wrapper = mountWithTheme( + + + + ); + }, 'Should render'); + + // Since only the geocoder_dataset is present, nothing should be rendered except the TooltipConfig + t.equal(wrapper.find(TooltipConfig).length, 1, 'Should render 1 TooltipConfig'); + t.equal(wrapper.find(DatasetTag).length, 0, 'Should render 1 DatasetTag'); + t.equal(wrapper.find(FieldSelector).length, 0, 'Should render 1 FieldSelector'); + t.equal(wrapper.find(ChickletedInput).length, 0, 'Should render 1 ChickletedInput'); + + t.end(); +}); diff --git a/test/helpers/mock-state.js b/test/helpers/mock-state.js index 53fb4fbb69..523f46b3d2 100644 --- a/test/helpers/mock-state.js +++ b/test/helpers/mock-state.js @@ -56,6 +56,7 @@ import tripGeojson, {tripDataInfo} from 'test/fixtures/trip-geojson'; import {processCsvData, processGeojson} from 'processors/data-processor'; import {COMPARE_TYPES} from 'constants/tooltip'; import {MOCK_MAP_STYLE} from './mock-map-styles'; +import {getUpdateVisDataPayload} from 'components/geocoder-panel'; const geojsonFields = cloneDeep(fields); const geojsonRows = cloneDeep(rows); @@ -457,6 +458,52 @@ function mockStateWithTooltipFormat() { return prepareState; } +function mockStateWithGeocoderDataset() { + const initialState = cloneDeep(InitialState); + + const oldInteractionConfig = initialState.visState.interactionConfig.tooltip; + const newInteractionConfig = { + ...oldInteractionConfig, + config: { + ...oldInteractionConfig.config, + fieldsToShow: { + ...oldInteractionConfig.config.fieldsToShow, + geocoder_dataset: [ + { + name: 'lt', + format: null + }, + { + name: 'ln', + format: null + }, + { + name: 'icon', + format: null + }, + { + name: 'text', + format: null + } + ] + }, + compareMode: false, + compareType: COMPARE_TYPES.ABSOLUTE + } + }; + const geocoderDataset = getUpdateVisDataPayload(48.85658, 2.35183, 'Paris'); + + const prepareState = applyActions(keplerGlReducer, initialState, [ + { + action: VisStateActions.updateVisData, + payload: geocoderDataset + }, + {action: VisStateActions.interactionConfigChange, payload: [newInteractionConfig]} + ]); + + return prepareState; +} + // saved hexagon layer export const expectedSavedLayer0 = { id: 'hexagon-2', @@ -747,6 +794,7 @@ export const StateWTrips = mockStateWithTripData(); export const StateWTripGeojson = mockStateWithTripGeojson(); export const StateWTooltipFormat = mockStateWithTooltipFormat(); export const StateWH3Layer = mockStateWithH3Layer(); +export const StateWithGeocoderDataset = mockStateWithGeocoderDataset(); export const expectedSavedTripLayer = { id: 'trip-0', diff --git a/test/node/utils/index.js b/test/node/utils/index.js index 9704756b9f..bd1f4cf676 100644 --- a/test/node/utils/index.js +++ b/test/node/utils/index.js @@ -34,3 +34,4 @@ import './color-util-test'; import './util-test'; import './export-utils-test'; import './s2-utils-test'; +import './kepler-gl-utils-test'; diff --git a/test/node/utils/kepler-gl-utils-test.js b/test/node/utils/kepler-gl-utils-test.js new file mode 100644 index 0000000000..641a33f816 --- /dev/null +++ b/test/node/utils/kepler-gl-utils-test.js @@ -0,0 +1,48 @@ +// Copyright (c) 2021 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import test from 'tape'; +import {GEOCODER_DATASET_NAME} from 'constants/default-settings'; +import {getVisibleDatasets} from 'components/kepler-gl'; + +test('kepler-gl utils -> getVisibleDatasets', t => { + // Geocoder dataset mock can be an empty object since the filter function only cares about the key + // in the 'datasets' object and filters by it + const datasets = { + first: {}, + second: {}, + geocoder_dataset: {} + }; + + t.true( + datasets[GEOCODER_DATASET_NAME], + `${GEOCODER_DATASET_NAME} key should exist before being filtered` + ); + + const filteredResults = getVisibleDatasets(datasets); + + t.isEqual( + filteredResults[GEOCODER_DATASET_NAME], + undefined, + `Should not exist after filtering out ${GEOCODER_DATASET_NAME} key` + ); + + t.end(); +});