diff --git a/docs/api-reference/localization/README.md b/docs/api-reference/localization/README.md index bbace21b2b..2f181e6b4d 100644 --- a/docs/api-reference/localization/README.md +++ b/docs/api-reference/localization/README.md @@ -41,7 +41,7 @@ const reducers = combineReducers({ Let's say we want to add the Swedish language to kepler.gl. Easiest way to add translation of new language is to follow these 3 steps: - Find out the [language code][language-codes] for Swedish: `sv` -- Add new translation file `src/localization/sv.js` by copying `src/localization/en.js` and translating the strings +- Add new translation file `src/localization/translations/sv.js` by copying `src/localization/translations/en.js` and translating the strings - Update _LOCALES_ in `src/localization/locales.js` to include new language translation: ```javascript diff --git a/examples/demo-app/src/cloud-providers/carto/carto-provider.js b/examples/demo-app/src/cloud-providers/carto/carto-provider.js index 3fbff16b8d..23f9ea61bf 100644 --- a/examples/demo-app/src/cloud-providers/carto/carto-provider.js +++ b/examples/demo-app/src/cloud-providers/carto/carto-provider.js @@ -23,7 +23,7 @@ import Console from 'global/console'; import CartoIcon from './carto-icon'; import {formatCsv} from 'kepler.gl/processors'; import {Provider} from 'kepler.gl/cloud-providers'; -import {createDataContainer} from 'kepler.gl'; +import {createDataContainer} from 'kepler.gl/utils'; const NAME = 'carto'; const DISPLAY_NAME = 'CARTO'; diff --git a/examples/webpack.config.local.js b/examples/webpack.config.local.js index 1b081910e3..64722f1944 100644 --- a/examples/webpack.config.local.js +++ b/examples/webpack.config.local.js @@ -104,10 +104,7 @@ function makeLocalDevConfig(env, EXAMPLE_DIR = LIB_DIR, externals = {}) { return { // suppress warnings about bundle size devServer: { - historyApiFallback: true, - stats: { - warnings: false - } + historyApiFallback: true }, devtool: 'source-map', diff --git a/package.json b/package.json index 884ae23b5f..facabdcdb5 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "description": "kepler.gl is a webgl based application to visualize large scale location data in the browser", "license": "MIT", "main": "dist/index.js", + "types": "./types.d.ts", "keywords": [ "babel", "es6", @@ -43,11 +44,12 @@ "start:e2e": "npm run install-and-start -- examples/demo-app start-local-e2e", "build": "rm -fr dist && babel src --out-dir dist --source-maps inline", "build:umd": "webpack --config ./webpack/umd.js --progress --env.prod", + "build:types": "webpack --config ./webpack/build_types.js --progress", "analyze": "npm run analyze:bundle", "analyze:bundle": "webpack --config ./webpack/bundle.js --progress --env.prod", "check-licence": "uber-licence --dry", "add-licence": "uber-licence", - "prepublish": "uber-licence && yarn build && yarn build:umd", + "prepublish": "yarn build && yarn build:umd", "docs": "babel-node ./scripts/documentation.js", "typedoc": "typedoc --theme markdown --out typedoc --inputFiles ./src/reducers --inputFiles ./src/actions --excludeExternals --excludeNotExported --excludePrivate", "example-version": "babel-node ./scripts/edit-version.js", @@ -196,6 +198,7 @@ "babel-tape-runner": "^3.0.0", "babelify": "^10.0.0", "documentation": "^9.1.1", + "dts-bundle-webpack": "^1.0.2", "enzyme": "^3.9.0", "enzyme-adapter-react-16": "^1.10.0", "eslint": "^5.16.0", @@ -283,6 +286,7 @@ "Giuseppe Macri " ], "volta": { - "node": "12.19.0" + "node": "12.19.0", + "yarn": "1.22.17" } -} +} \ No newline at end of file diff --git a/src/actions/ui-state-actions.d.ts b/src/actions/ui-state-actions.d.ts index 4530d15d4c..a01c674582 100644 --- a/src/actions/ui-state-actions.d.ts +++ b/src/actions/ui-state-actions.d.ts @@ -198,3 +198,11 @@ export type SetLocaleUpdaterAction = { export function setLocale( locale: string ): Merge; + +/** TOGGLE_LAYER_PANEL_LIST_VIEW */ +export type ToggleLayerPanelListViewAction = { + payload: string; +} +export function toggleLayerPanelListView( + listView: string +): Merge; diff --git a/src/actions/ui-state-actions.js b/src/actions/ui-state-actions.js index 3dedab30e2..ab200cf996 100644 --- a/src/actions/ui-state-actions.js +++ b/src/actions/ui-state-actions.js @@ -270,3 +270,15 @@ export const setLocale = createAction(ActionTypes.SET_LOCALE, locale => ({ // @ts-ignore const uiStateActions = null; /* eslint-enable no-unused-vars */ + +/** + * Toggle layer panel list view + * @memberof uiStateActions + * @param listView layer panel listView value. Can be 'list' or 'sortByDataset' + * @type {typeof import('./ui-state-actions').toggleLayerPanelListView} + * @public + */ +export const toggleLayerPanelListView = createAction( + ActionTypes.TOGGLE_LAYER_PANEL_LIST_VIEW, + listView => listView +); diff --git a/src/actions/vis-state-actions.d.ts b/src/actions/vis-state-actions.d.ts index ea33b8e07d..8b2e3cac08 100644 --- a/src/actions/vis-state-actions.d.ts +++ b/src/actions/vis-state-actions.d.ts @@ -157,9 +157,11 @@ export function addFilter( export type AddLayerUpdaterAction = { config: object; + datasetId?: string; }; export function addLayer( - config: object + config: object, + datasetId?: string, ): Merge; export type ReorderLayerUpdaterAction = { diff --git a/src/actions/vis-state-actions.js b/src/actions/vis-state-actions.js index 656e9154e1..4e11e5aeeb 100644 --- a/src/actions/vis-state-actions.js +++ b/src/actions/vis-state-actions.js @@ -232,14 +232,16 @@ export function addFilter(dataId) { * Add a new layer * @memberof visStateActions * @param config - new layer config + * @param datasetId - dataset id used for creating an empty layer * @returns action * @type {typeof import('./vis-state-actions').addLayer} * @public */ -export function addLayer(config) { +export function addLayer(config, datasetId) { return { type: ActionTypes.ADD_LAYER, - config + config, + datasetId }; } diff --git a/src/components/common/action-panel.d.ts b/src/components/common/action-panel.d.ts index a0980ed009..3076cf1b5c 100644 --- a/src/components/common/action-panel.d.ts +++ b/src/components/common/action-panel.d.ts @@ -1,9 +1,9 @@ -import {FunctionComponent, PropsWithChildren, ComponentType, CSSProperties} from 'react'; +import {FunctionComponent, PropsWithChildren, ElementType, CSSProperties} from 'react'; export type ActionPanelItemProps = PropsWithChildren<{ color?: string, className?: string, - Icon?: ComponentType, + Icon?: ElementType, label: string, onClick?: () => void, isSelection?: boolean, diff --git a/src/components/common/checkbox.d.ts b/src/components/common/checkbox.d.ts new file mode 100644 index 0000000000..4e12c64f58 --- /dev/null +++ b/src/components/common/checkbox.d.ts @@ -0,0 +1,3 @@ +export const Checkbox: (...props: any[]) => JSX.Element; + +export default Checkbox; diff --git a/src/components/common/file-uploader/file-drop.d.ts b/src/components/common/file-uploader/file-drop.d.ts index 765a0ee295..e0cdb43454 100644 --- a/src/components/common/file-uploader/file-drop.d.ts +++ b/src/components/common/file-uploader/file-drop.d.ts @@ -15,5 +15,5 @@ export type FileDropProps = { onFrameDrop?: (event: any) => void; }; -const FileDrop: React.FunctionComponent; +export const FileDrop: React.FunctionComponent; export default FileDrop; diff --git a/src/components/common/file-uploader/upload-button.d.ts b/src/components/common/file-uploader/upload-button.d.ts new file mode 100644 index 0000000000..1ffd85da2f --- /dev/null +++ b/src/components/common/file-uploader/upload-button.d.ts @@ -0,0 +1,8 @@ +import React from 'react'; + +export type UploadButtonProps = { + onUpload: (files: FileList, event: any) => void; +}; + +export const UploadButton: React.FC; +export default UploadButton; diff --git a/src/components/common/file-uploader/upload-button.d.ts.wip b/src/components/common/file-uploader/upload-button.d.ts.wip deleted file mode 100644 index f889aa0113..0000000000 --- a/src/components/common/file-uploader/upload-button.d.ts.wip +++ /dev/null @@ -1,11 +0,0 @@ -// For some reason these types don't work -// JSX element type 'UploadButton' does not have any construct or call signatures. - -import React from 'react'; - -export type UploadButtonProps = { - onUpload: (files: FileList, event: any) => void; -}; - -export const UploadButton: React.Component; -export default UploadButton; diff --git a/src/components/common/file-uploader/upload-button.js b/src/components/common/file-uploader/upload-button.js index 4565845f3b..78a4c6ffe9 100644 --- a/src/components/common/file-uploader/upload-button.js +++ b/src/components/common/file-uploader/upload-button.js @@ -18,15 +18,9 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import React, {Component, createRef} from 'react'; +import React, {useCallback, useRef} from 'react'; import styled from 'styled-components'; -/** -@typedef {{ - onUpload: (files: FileList, event: any) => void; -}} UploadButtonProps -*/ - const Wrapper = styled.div` display: inline-block; color: ${props => props.theme.textColorLT}; @@ -38,46 +32,53 @@ const Wrapper = styled.div` font-weight: 500; } `; +const inputStyle = {display: 'none'}; + /* Inspired by https://github.com/okonet/react-dropzone/blob/master/src/index.js */ -/** @augments React.Component */ -export class UploadButton extends Component { - _fileInput = createRef(); +/** @type {typeof import('./upload-button').UploadButton} */ +const UploadButton = ({onUpload, children}) => { + const _fileInput = useRef(null); - _onClick = () => { - this._fileInput.current.value = null; - this._fileInput.current.click(); - }; + const _onClick = useCallback(() => { + if (_fileInput.current) { + // @ts-ignore create ref with useRef + _fileInput.current.value = null; + // @ts-ignore create ref with useRef + _fileInput.current.click(); + } + }, [_fileInput]); - _onChange = event => { - const { - target: {files} - } = event; + const _onChange = useCallback( + event => { + const { + target: {files} + } = event; - if (!files) { - return; - } + if (!files) { + return; + } - this.props.onUpload(files, event); - }; + onUpload(files, event); + }, + [onUpload] + ); - render() { - return ( - - - - {this.props.children} - - - ); - } -} + return ( + + + + {children} + + + ); +}; export default UploadButton; diff --git a/src/components/common/icons/index.d.ts b/src/components/common/icons/index.d.ts new file mode 100644 index 0000000000..7da7df421e --- /dev/null +++ b/src/components/common/icons/index.d.ts @@ -0,0 +1,76 @@ +import React from 'react'; + +export const Add: React.ElementType; +export const AnchorWindow: React.ElementType; +export const ArrowDown: React.ElementType; +export const ArrowDownAlt: React.ElementType; +export const ArrowDownSolid: React.ElementType; +export const ArrowLeft: React.ElementType; +export const ArrowRight: React.ElementType; +export const ArrowUpSolid: React.ElementType; +export const ArrowUpAlt: React.ElementType; +export const ArrowUp: React.ElementType; +export const Base: React.ElementType; +export const Bug: React.ElementType; +export const Cancel: React.ElementType; +export const Checkmark: React.ElementType; +export const Clipboard: React.ElementType; +export const Clock: React.ElementType; +export const Close: React.ElementType; +export const Copy: React.ElementType; +export const Crosshairs: React.ElementType; +export const Cube3d: React.ElementType; +export const CursorClick: React.ElementType; +export const DataTable: React.ElementType; +export const Db: React.ElementType; +export const Delete: React.ElementType; +export const Docs: React.ElementType; +export const DragNDrop: React.ElementType; +export const Email: React.ElementType; +export const Expand: React.ElementType; +export const EyeSeen: React.ElementType; +export const EyeUnseen: React.ElementType; +export const File: React.ElementType; +export const Files: React.ElementType; +export const FileType: React.ElementType; +export const FilterFunnel: React.ElementType; +export const FreeWindow: React.ElementType; +export const Gear: React.ElementType; +export const Hash: React.ElementType; +export const Histogram: React.ElementType; +export const Info: React.ElementType; +export const Layers: React.ElementType; +export const LeftArrow: React.ElementType; +export const Legend: React.ElementType; +export const LineChart: React.ElementType; +export const Logout: React.ElementType; +export const Login: React.ElementType; +export const Map: React.ElementType; +export const MapIcon: React.ElementType; +export const Minus: React.ElementType; +export const Messages: React.ElementType; +export const Pause: React.ElementType; +export const Picture: React.ElementType; +export const Pin: React.ElementType; +export const Play: React.ElementType; +export const Reduce: React.ElementType; +export const Reset: React.ElementType; +export const Rocket: React.ElementType; +export const Save: React.ElementType; +export const Save2: React.ElementType; +export const Share: React.ElementType; +export const SquareSelect: React.ElementType; +export const Settings: React.ElementType; +export const Search: React.ElementType; +export const Split: React.ElementType; +export const Table: React.ElementType; +export const Trash: React.ElementType; +export const Upload: React.ElementType; +export const VertDots: React.ElementType; +export const VertThreeDots: React.ElementType; +export const IconWrapper: React.ElementType; +export const CodeAlt: React.ElementType; +export const Warning: React.ElementType; +export const DrawPolygon: React.ElementType; +export const Polygon: React.ElementType; +export const Rectangle: React.ElementType; diff --git a/src/components/common/icons/index.js b/src/components/common/icons/index.js index b952125593..5ff0993f99 100644 --- a/src/components/common/icons/index.js +++ b/src/components/common/icons/index.js @@ -92,3 +92,5 @@ export {default as Warning} from './warning'; export {default as DrawPolygon} from './draw-polygon'; export {default as Polygon} from './polygon'; export {default as Rectangle} from './rectangle'; +export {default as OrderByList} from './order-by-list'; +export {default as OrderByDataset} from './order-by-dataset'; diff --git a/src/components/common/icons/order-by-dataset.js b/src/components/common/icons/order-by-dataset.js new file mode 100644 index 0000000000..85993b5eba --- /dev/null +++ b/src/components/common/icons/order-by-dataset.js @@ -0,0 +1,35 @@ +import React from 'react'; +import Base from './base'; +import PropTypes from 'prop-types'; + +const OrderByDataset = props => ( + + + + + +); +OrderByDataset.propTypes = { + /** Set the height of the icon, ex. '16px' */ + height: PropTypes.string, + fill: PropTypes.string +}; +OrderByDataset.defaultProps = { + height: '20px', + fill: 'currentColor', + viewBox: '0 0 24 24', + predefinedClassName: 'data-ex-icons-order-by-dataset' +}; + +export default OrderByDataset; diff --git a/src/components/common/icons/order-by-list.js b/src/components/common/icons/order-by-list.js new file mode 100644 index 0000000000..90acf6b140 --- /dev/null +++ b/src/components/common/icons/order-by-list.js @@ -0,0 +1,25 @@ +import React from 'react'; +import Base from './base'; +import PropTypes from 'prop-types'; + +const OrderByList = props => ( + + + +); +OrderByList.propTypes = { + /** Set the height of the icon, ex. '16px' */ + height: PropTypes.string, + fill: PropTypes.string +}; +OrderByList.defaultProps = { + height: '20px', + fill: 'currentColor', + viewBox: '0 0 24 24', + predefinedClassName: 'data-ex-icons-order-by-list' +}; + +export default OrderByList; diff --git a/src/components/common/styled-components.d.ts b/src/components/common/styled-components.d.ts new file mode 100644 index 0000000000..f9c1771268 --- /dev/null +++ b/src/components/common/styled-components.d.ts @@ -0,0 +1,48 @@ +export type ReactElement = (...props: any[]) => JSX.Element; + +export const Tooltip: ReactElement; +export const BottomWidgetInner: ReactElement; +export const CenterFlexbox: ReactElement; +export const DatasetSquare: ReactElement; +export const TruncatedTitleText: ReactElement; +export const Button: ReactElement; +export const Input: ReactElement; +export const PanelLabel: ReactElement; +export const StyledFilterContent: ReactElement; +export const PanelLabel: ReactElement; +export const SidePanelSection: ReactElement; +export const SelectTextBold: ReactElement; +export const IconRoundSmall: ReactElement; +export const StyledAttrbution: ReactElement; +export const MapControlButton: ReactElement; +export const IconRoundSmall: ReactElement; +export const StyledModalContent: ReactElement; +export const InputLight: ReactElement; +export const StyledMapContainer: ReactElement; +export const StyledModalVerticalPanel: ReactElement; +export const StyledModalSection: ReactElement; +export const CenterVerticalFlexbox: ReactElement; +export const CheckMark: ReactElement; +export const StyledExportSection: ReactElement; +export const StyledFilteredOption: ReactElement; +export const StyledModalContent: ReactElement; +export const StyledType: ReactElement; +export const SelectionButton: ReactElement; +export const TextAreaLight: ReactElement; +export const StyledModalInputFootnote: ReactElement; +export const SidePanelDivider: ReactElement; +export const StyledPanelHeader: ReactElement; +export const PanelHeaderTitle: ReactElement; +export const PanelHeaderContent: ReactElement; +export const PanelContent: ReactElement; +export const SBFlexboxNoMargin: ReactElement; +export const StyledPanelDropdown: ReactElement; +export const InlineInput: ReactElement; +export const SBFlexboxItem: ReactElement; +export const SpaceBetweenFlexbox: ReactElement; +export const PanelLabelWrapper: ReactElement; +export const PanelLabelBold: ReactElement; +export const PanelHeaderContent: ReactElement; + + + diff --git a/src/components/container.d.ts b/src/components/container.d.ts new file mode 100644 index 0000000000..62f48e8ebf --- /dev/null +++ b/src/components/container.d.ts @@ -0,0 +1,8 @@ +import {Component} from 'react'; +import {InjectorType, ProvideRecipesToInjectorType} from './injector'; + +export const appInjector: InjectorType; + +export const injectComponents: (recipes: any[]) => ProvideRecipesToInjectorType; + +export const ContainerFactory: (KeplerGl: Component) => Component; 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/index.d.ts b/src/components/index.d.ts new file mode 100644 index 0000000000..292ebff4ad --- /dev/null +++ b/src/components/index.d.ts @@ -0,0 +1,49 @@ +import React from 'react'; +import {FeatureFlags} from './context'; + +export {withState} from './injector'; + +export * from './context'; +export * from './bottom-widget'; +export * from './kepler-gl'; +export * from './map-container'; +export * from './maps-layout'; +export * from './modal-container'; +export * from './side-panel'; +export * from './container'; + +// TODO: we need more specific types for the following components +export * from './map/map-legend'; +export * from './map/split-map-button'; +export * from './side-panel/common/dataset-tag'; +export * from './common/checkbox'; +export * from './common/styled-components'; +export * from './common/tippy-tooltip' +export * as Icons from './common/icons'; +export * from './common/file-uploader/file-drop'; +export * from './common/file-uploader/upload-button'; +export {default as Portaled} from './common/portaled'; + +export const PanelHeaderAction: (...props: any[]) => JSX.Element; +export const TippyTooltip: (...props: any[]) => JSX.Element; +export const Toggle3dButtonFactory: (...deps: any) => React.ElementType; +export const ToggleGlobeButtonFactory: (...deps: any) => React.ElementType; +export const MapsLayoutFactory: (...deps: any) => React.ElementType; +export const PanelHeaderDropdownFactory: (...deps: any) => React.ElementType; +export const NotificationItemFactory: (...deps: any) => React.ElementType; +export const DropdownList: React.ElementType; +export const VerticalToolbar: React.ElementType; +export const ToolbarItem: React.ElementType; +export const ModalTitle: React.ElementType; +export const ItemSelector: React.ElementType; +export const Slider: React.ElementType; +export const LoadingSpinner: React.ElementType; +export const LayerConfigGroup: React.ElementType; +export const ChannelByValueSelector: React.ElementType; +export const LayerColorRangeSelector: React.ElementType; +export const LayerColorSelector: React.ElementType; +export const VisConfigSlider: React.ElementType; +export const ConfigGroupCollapsibleContent: React.ElementType; +export const PanelLabel: React.ElementType; + +export const useFeatureFlags: () => FeatureFlags; diff --git a/src/components/index.js b/src/components/index.js index ba41a9b289..b24959d106 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -59,12 +59,21 @@ export {CollapseButtonFactory, default as SidebarFactory} from './side-panel/sid export {default as PanelToggleFactory} from './side-panel/panel-toggle'; export {default as PanelTabFactory} from './side-panel/panel-tab'; -export {AddDataButtonFactory, default as LayerManagerFactory} from './side-panel/layer-manager'; +export {default as LayerManagerFactory} from './side-panel/layer-manager'; export {default as LayerPanelFactory} from './side-panel/layer-panel/layer-panel'; export {default as LayerPanelHeaderFactory} from './side-panel/layer-panel/layer-panel-header'; export {default as LayerConfiguratorFactory} from './side-panel/layer-panel/layer-configurator'; export {default as TextLabelPanelFactory} from './side-panel/layer-panel/text-label-panel'; export {LayerConfigGroupLabelFactory} from './side-panel/layer-panel/layer-config-group'; +export { + AddDataButtonFactory, + default as DatasetSectionFactory +} from './side-panel/layer-panel/dataset-section'; +export {default as DatasetLayerSectionFactory} from './side-panel/layer-panel/dataset-layer-section'; +export {default as DatasetLayerGroupFactory} from './side-panel/layer-panel/dataset-layer-group'; +export {default as PanelViewListToggleFactory} from './side-panel/layer-panel/panel-view-list-toggle'; +export {default as AddLayerButtonFactory} from './side-panel/layer-panel/add-layer-button'; +export {default as LayerListFactory} from './side-panel/layer-panel/layer-list'; export {default as SourceDataCatalogFactory} from './side-panel/common/source-data-catalog'; export {default as SourceDataSelectorFactory} from './side-panel/common/source-data-selector'; diff --git a/src/components/injector.d.ts b/src/components/injector.d.ts new file mode 100644 index 0000000000..deb31b343e --- /dev/null +++ b/src/components/injector.d.ts @@ -0,0 +1,26 @@ +import {Component, FunctionComponent, FC} from 'react'; + +export const ERROR_MSG: { + wrongRecipeType: string; + noDep: (fac: any, parent: any) => string; + noFunc: string; +}; + +export type FactoryElement = (...args) => Component; +export type Factory = FactoryElement & { + deps: FactoryElement[]; +} +export type InjectorType = { + provide: (factory: any, replacement: any) => InjectorType; + get: (fac: any, parent?: any) => any; +} + +export const injector: (map?: Map) => InjectorType; + +export type ProvideRecipesToInjectorType = (factories: Factory[], appInjector: InjectorType) => InjectorType; + +export const provideRecipesToInjector: ProvideRecipesToInjectorType; + +export const flattenDeps: (deps: Factory[], replacement: any) => Factory[]; + +export const withState: (lenses: any, mapStateToProps: (state: object) => object, actions: object) => (Component: Component | FunctionComponent | FC) => Component; diff --git a/src/components/kepler-gl.js b/src/components/kepler-gl.js index 51b25586f9..ea7e232227 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,7 +139,12 @@ export const mapFieldsSelector = props => ({ locale: props.uiState.locale }); -export const sidePanelSelector = (props, availableProviders) => ({ +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); +} + +export const sidePanelSelector = (props, availableProviders, filteredDatasets) => ({ appName: props.appName, version: props.version, appWebsite: props.appWebsite, @@ -149,7 +155,7 @@ export const sidePanelSelector = (props, availableProviders) => ({ visStateActions: props.visStateActions, uiStateActions: props.uiStateActions, - datasets: props.visState.datasets, + datasets: filteredDatasets, filters: props.visState.filters, layers: props.visState.layers, layerOrder: props.visState.layerOrder, @@ -312,6 +318,9 @@ function KeplerGlFactory( : theme ); + datasetsSelector = props => props.visState.datasets; + filteredDatasetsSelector = createSelector(this.datasetsSelector, getVisibleDatasets); + availableProviders = createSelector( props => props.cloudProviders, providers => @@ -382,7 +391,8 @@ function KeplerGlFactory( const availableProviders = this.availableProviders(this.props); const mapFields = mapFieldsSelector(this.props); - const sideFields = sidePanelSelector(this.props, availableProviders); + const filteredDatasets = this.filteredDatasetsSelector(this.props); + const sideFields = sidePanelSelector(this.props, availableProviders, filteredDatasets); const plotContainerFields = plotContainerSelector(this.props); const bottomWidgetFields = bottomWidgetSelector(this.props, theme); const modalContainerFields = modalContainerSelector(this.props, this.root.current); diff --git a/src/components/map-container.d.ts b/src/components/map-container.d.ts index eb90b3bb0c..798870c2aa 100644 --- a/src/components/map-container.d.ts +++ b/src/components/map-container.d.ts @@ -1,13 +1,5 @@ -import {Layer} from '../layers'; -import {SplitMapLayers} from '../reducers/vis-state-updaters'; import React from 'react'; -export function prepareLayersToRender( - layers: Layer[], - layerData: any[], - mapLayers: SplitMapLayers | undefined -): {[id: string]: boolean}; - export type MapContainerProps = { // TODO primary: boolean; diff --git a/src/components/map/map-control-tooltip.js b/src/components/map/map-control-tooltip.js index 8997202520..a605d5d87a 100644 --- a/src/components/map/map-control-tooltip.js +++ b/src/components/map/map-control-tooltip.js @@ -19,7 +19,7 @@ // THE SOFTWARE. import React from 'react'; -import {Tooltip} from '..'; +import {Tooltip} from 'components/common/styled-components'; import {FormattedMessage} from 'localization'; function MapControlTooltipFactory() { diff --git a/src/components/modal-container.js b/src/components/modal-container.js index f7af49aba0..4dd6f02ac9 100644 --- a/src/components/modal-container.js +++ b/src/components/modal-container.js @@ -178,7 +178,8 @@ export default function ModalContainerFactory( _onExportImage = () => { if (!this.props.uiState.exportImage.processing) { - exportImage(this.props, `${this.props.appName}.png`); + // @ts-ignore TODO: fix exportImage method + exportImage(this.props.uiState.exportImage, `${this.props.appName}.png`); this.props.uiStateActions.cleanupExportImage(); this._closeModal(); } diff --git a/src/components/side-panel.js b/src/components/side-panel.js index e1eeae6313..a90f25d7d4 100644 --- a/src/components/side-panel.js +++ b/src/components/side-panel.js @@ -212,9 +212,11 @@ export default function SidePanelFactory( />
- - - + {currentPanel.id !== 'layer' ? ( + + + + ) : null} {PanelComponent ? ( ) : null} diff --git a/src/components/side-panel/common/dataset-tag.d.ts b/src/components/side-panel/common/dataset-tag.d.ts new file mode 100644 index 0000000000..0fc9d6c552 --- /dev/null +++ b/src/components/side-panel/common/dataset-tag.d.ts @@ -0,0 +1,3 @@ +import React from 'react'; + +export const DatasetTagFactory: (...deps: any) => React.ElementType; 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 : ( + + ) + )} { - const labeledLayerBlendings = Object.keys(LAYER_BLENDINGS).reduce( - (acc, current) => ({ - ...acc, - [intl.formatMessage({id: LAYER_BLENDINGS[current].label})]: current - }), - {} - ); - - const onChange = useCallback(blending => updateLayerBlending(labeledLayerBlendings[blending]), [ - updateLayerBlending, - labeledLayerBlendings - ]); - - return ( - - - - - - - ); -}; - -// make sure the element is always visible while is being dragged -// item being dragged is appended in body, here to reset its global style -const SortableStyledItem = styled.div` - z-index: ${props => props.theme.dropdownWrapperZ + 1}; - - &.sorting { - pointer-events: none; - } +import styled from 'styled-components'; - &.sorting-layers .layer-panel__header { - background-color: ${props => props.theme.panelBackgroundHover}; - font-family: ${props => props.theme.fontFamily}; - font-weight: ${props => props.theme.fontWeight}; - font-size: ${props => props.theme.fontSize}; - line-height: ${props => props.theme.lineHeight}; - *, - *:before, - *:after { - box-sizing: border-box; - } - .layer__drag-handle { - opacity: 1; - color: ${props => props.theme.textColorHl}; - } - } +import LayerListFactory from './layer-panel/layer-list'; +import DatasetLayerGroupFactory from './layer-panel/dataset-layer-group'; +import PanelViewListToggleFactory from './layer-panel/panel-view-list-toggle'; +import PanelTitleFactory from './panel-title'; +import DatasetSectionFactory from './layer-panel/dataset-section'; +import AddLayerButtonFactory from './layer-panel/add-layer-button'; + +import {SidePanelDivider, SidePanelSection} from 'components/common/styled-components'; + +const LayerHeader = styled.div.attrs({ + className: 'layer-manager-header' +})` + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-top: 16px; `; -export function AddDataButtonFactory() { - const AddDataButton = ({onClick, isInactive}) => ( - - ); - - return AddDataButton; -} - -LayerManagerFactory.deps = [AddDataButtonFactory, LayerPanelFactory, SourceDataCatalogFactory]; - -function LayerManagerFactory(AddDataButton, LayerPanel, SourceDataCatalog) { - // By wrapping layer panel using a sortable element we don't have to implement the drag and drop logic into the panel itself; - // Developers can provide any layer panel implementation and it will still be sortable - const SortableItem = SortableElement(({children, isSorting}) => { - return ( - - {children} - - ); - }); - - const WrappedSortableContainer = SortableContainer(({children}) => { - return
{children}
; - }); - +LayerManagerFactory.deps = [ + LayerListFactory, + DatasetLayerGroupFactory, + PanelViewListToggleFactory, + PanelTitleFactory, + DatasetSectionFactory, + AddLayerButtonFactory +]; + +function LayerManagerFactory( + LayerList, + DatasetLayerGroup, + PanelViewListToggle, + PanelTitle, + DatasetSection, + AddLayerButton +) { class LayerManager extends Component { static propTypes = { datasets: PropTypes.object.isRequired, @@ -147,128 +72,94 @@ function LayerManagerFactory(AddDataButton, LayerPanel, SourceDataCatalog) { showDatasetTable: PropTypes.func.isRequired, updateTableColor: PropTypes.func.isRequired }; - state = { - isSorting: false - }; - - layerClassSelector = props => props.layerClasses; - layerTypeOptionsSelector = createSelector(this.layerClassSelector, layerClasses => - Object.keys(layerClasses).map(key => { - const layer = new layerClasses[key](); - return { - id: key, - label: layer.name, - icon: layer.layerIcon, - requireData: layer.requireData - }; - }) - ); - - _addEmptyNewLayer = () => { - const {visStateActions} = this.props; - visStateActions.addLayer(); - }; - _handleSort = ({oldIndex, newIndex}) => { + _addEmptyNewLayer = dataset => { const {visStateActions} = this.props; - visStateActions.reorderLayer(arrayMove(this.props.layerOrder, oldIndex, newIndex)); - this.setState({isSorting: false}); + visStateActions.addLayer(undefined, dataset); }; - _onSortStart = () => { - this.setState({isSorting: true}); - }; - - _updateBeforeSortStart = ({index}) => { - // if layer config is active, close it - const {layerOrder, layers, visStateActions} = this.props; - const layerIdx = layerOrder[index]; - if (layers[layerIdx].config.isConfigActive) { - visStateActions.layerConfigChange(layers[layerIdx], {isConfigActive: false}); - } + _toggleLayerPanelListView = listView => { + const {uiStateActions} = this.props; + uiStateActions.toggleLayerPanelListView(listView); }; render() { const { layers, datasets, + intl, layerOrder, showAddDataModal, updateTableColor, showDatasetTable, removeDataset, uiStateActions, - visStateActions + visStateActions, + layerPanelListView, + panelMetadata } = this.props; - const {toggleModal: openModal} = uiStateActions; - const defaultDataset = Object.keys(datasets)[0]; - const layerTypeOptions = this.layerTypeOptionsSelector(this.props); - - const layerActions = { - layerColorUIChange: visStateActions.layerColorUIChange, - layerConfigChange: visStateActions.layerConfigChange, - layerVisualChannelConfigChange: visStateActions.layerVisualChannelConfigChange, - layerTypeChange: visStateActions.layerTypeChange, - layerVisConfigChange: visStateActions.layerVisConfigChange, - layerTextLabelChange: visStateActions.layerTextLabelChange, - removeLayer: visStateActions.removeLayer, - duplicateLayer: visStateActions.duplicateLayer - }; - const panelProps = { - datasets, - openModal, - layerTypeOptions - }; + const defaultDataset = Object.keys(datasets)[0]; + const isSortByDatasetMode = layerPanelListView === 'sortByDataset'; return (
- + + + - - - {layerOrder.map( - (layerIdx, index) => - !layers[layerIdx].config.hidden && ( - - - - ) - )} - + + + + + {defaultDataset ? ( + + ) : null} + - {defaultDataset ? ( - - ) : null} + {isSortByDatasetMode ? ( + + ) : ( + + )}
); diff --git a/src/components/side-panel/layer-panel/add-layer-button.d.ts b/src/components/side-panel/layer-panel/add-layer-button.d.ts new file mode 100644 index 0000000000..74983620a4 --- /dev/null +++ b/src/components/side-panel/layer-panel/add-layer-button.d.ts @@ -0,0 +1,12 @@ +import React from 'react'; +import {Datasets} from 'reducers/vis-state-updaters'; +import {IntlShape} from 'react-intl'; + +export type AddLayerButtonProps = { + datasets: Datasets; + onOptionSelected: (dataset) => {}; + typeaheadPlaceholder?: string; + intl: IntlShape; +}; + +export default function AddLayerButtonFactory(): React.Component; diff --git a/src/components/side-panel/layer-panel/add-layer-button.js b/src/components/side-panel/layer-panel/add-layer-button.js new file mode 100644 index 0000000000..a1aa746d9e --- /dev/null +++ b/src/components/side-panel/layer-panel/add-layer-button.js @@ -0,0 +1,201 @@ +// 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 React, {useCallback, useMemo, useState} from 'react'; +import styled from 'styled-components'; +import {FormattedMessage} from 'localization'; + +import Tippy from '@tippyjs/react'; +import {Add} from 'components/common/icons'; +import {Button} from 'components/common/styled-components'; +import {DatasetSquare} from 'components'; +import Typeahead from 'components/common/item-selector/typeahead'; +import Accessor from 'components/common/item-selector/accessor'; + +const DropdownContainer = styled.div.attrs({ + className: 'add-layer-menu-dropdown' +})` + .list-selector { + border-top: 1px solid ${props => props.theme.secondaryInputBorderColor}; + width: 100%; + /* disable scrolling, currently set to 280px internally */ + max-height: unset; + } + + .list__item > div { + display: flex; + flex-direction: row; + justify-content: flex-start; + line-height: 18px; + padding: 0; + + svg { + margin-right: 10px; + } + } +`; + +const AddLayerMenu = styled.div.attrs({ + className: 'add-layer-menu' +})` + display: flex; + flex-direction: column; + min-width: 240px; + max-width: 240px; + position: absolute; + top: 100%; + left: -53px; + z-index: 5; +`; + +const ListItemWrapper = styled.div.attrs({ + className: 'add-layer-menu-list-item-wrapper' +})` + display: flex; + color: ${props => props.theme.textColor}; + font-size: 11px; + letter-spacing: 0.2px; + overflow: auto; + + .dataset-color { + flex-shrink: 0; + margin-top: 3px; + } + + .dataset-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +`; + +const TYPEAHEAD_CLASS = 'typeahead'; +const TYPEAHEAD_INPUT_CLASS = 'typeahead__input'; + +function AddLayerButtonFactory() { + const ListItem = ({value}) => ( + + +
+ {value.label} +
+
+ ); + + const AddLayerButton = props => { + const {datasets, onOptionSelected, typeaheadPlaceholder, intl} = props; + const [showAddLayerDropdown, setShowAddLayerDropdown] = useState(false); + + const toggleAddLayerDropdown = useCallback(() => { + setShowAddLayerDropdown(!showAddLayerDropdown); + }, [showAddLayerDropdown, setShowAddLayerDropdown]); + + const _onBlur = useCallback(() => { + setShowAddLayerDropdown(false); + }, []); + + const toggleSelectedOption = useCallback( + option => { + onOptionSelected(option.value); + _onBlur(); + }, + [onOptionSelected, _onBlur] + ); + + const onButtonBlur = useCallback( + event => { + if ( + [TYPEAHEAD_CLASS, TYPEAHEAD_INPUT_CLASS].every( + cls => !event?.relatedTarget?.classList.contains(cls) + ) + ) { + _onBlur(); + } + }, + [_onBlur] + ); + + const onSearchBlur = useCallback(() => { + _onBlur(); + }, [_onBlur]); + + const options = useMemo(() => { + return Object.values(datasets).map(ds => ({ + label: ds.label, + value: ds.id, + color: ds.color + })); + }, [datasets]); + + return ( + + + + + + } + > + + + ); + }; + + return AddLayerButton; +} + +export default AddLayerButtonFactory; diff --git a/src/components/side-panel/layer-panel/column-selector.js b/src/components/side-panel/layer-panel/column-selector.js index e7d4b35a40..08f46bc72c 100644 --- a/src/components/side-panel/layer-panel/column-selector.js +++ b/src/components/side-panel/layer-panel/column-selector.js @@ -21,7 +21,7 @@ import React from 'react'; import styled from 'styled-components'; import {FormattedMessage} from 'localization'; -import {PanelLabel} from '../..'; +import {PanelLabel} from 'components/common/styled-components'; import FieldSelectorFactory from 'components/common/field-selector'; import {validateColumn} from 'reducers/vis-state-merger'; diff --git a/src/components/side-panel/layer-panel/dataset-layer-group.js b/src/components/side-panel/layer-panel/dataset-layer-group.js new file mode 100644 index 0000000000..75a96f9602 --- /dev/null +++ b/src/components/side-panel/layer-panel/dataset-layer-group.js @@ -0,0 +1,55 @@ +import React, {useMemo} from 'react'; + +import DatasetLayerSectionFactory from './dataset-layer-section'; + +DatasetLayerGroupFactory.deps = [DatasetLayerSectionFactory]; + +function DatasetLayerGroupFactory(DatasetLayerSection) { + const DatasetLayerGroup = props => { + const { + datasets, + showDatasetTable, + layers, + updateTableColor, + showDeleteDataset, + removeDataset, + layerOrder, + layerClasses, + uiStateActions, + visStateActions + } = props; + + const datasetLayerSectionData = useMemo(() => { + return Object.values(datasets).map(dataset => { + // Global layer order will contain the correct order of layers + // We just empty the positions in layers array (for each dataset) + // where the layer doesn't belong to a dataset and set it to null + const datasetLayers = layers.map(layer => + layer.config.dataId === dataset.id ? layer : null + ); + + return {dataset, datasetLayers}; + }); + }, [datasets, layers]); + + return datasetLayerSectionData.map(dlsData => ( + + )); + }; + + return DatasetLayerGroup; +} + +export default DatasetLayerGroupFactory; diff --git a/src/components/side-panel/layer-panel/dataset-layer-section.js b/src/components/side-panel/layer-panel/dataset-layer-section.js new file mode 100644 index 0000000000..d733ba34a2 --- /dev/null +++ b/src/components/side-panel/layer-panel/dataset-layer-section.js @@ -0,0 +1,59 @@ +import React, {useMemo} from 'react'; +import styled from 'styled-components'; + +import SourceDataCatalogFactory from '../common/source-data-catalog'; +import LayerListFactory from './layer-list'; + +const DatasetLayerSectionWrapper = styled.div.attrs({ + className: 'dataset-layer-section' +})` + margin-bottom: 16px; +`; + +DatasetLayerSectionFactory.deps = [SourceDataCatalogFactory, LayerListFactory]; + +function DatasetLayerSectionFactory(SourceDataCatalog, LayerList) { + const DatasetLayerSection = props => { + const { + dataset, + showDatasetTable, + layers, + updateTableColor, + showDeleteDataset, + removeDataset, + layerOrder, + layerClasses, + uiStateActions, + visStateActions + } = props; + + const datasets = useMemo(() => { + return {[dataset.id]: dataset}; + }, [dataset]); + + return ( + + + + + ); + }; + + return DatasetLayerSection; +} + +export default DatasetLayerSectionFactory; diff --git a/src/components/side-panel/layer-panel/dataset-section.js b/src/components/side-panel/layer-panel/dataset-section.js new file mode 100644 index 0000000000..a01e34a72f --- /dev/null +++ b/src/components/side-panel/layer-panel/dataset-section.js @@ -0,0 +1,81 @@ +import React from 'react'; +import styled from 'styled-components'; +import {FormattedMessage} from 'localization'; +import {Add} from 'components/common/icons'; +import {Button} from 'components/common/styled-components'; + +import SourceDataCatalogFactory from '../common/source-data-catalog'; + +const StyledDatasetTitle = styled.div` + line-height: ${props => props.theme.sidePanelTitleLineHeight}; + font-weight: 400; + letter-spacing: 1.25px; + color: ${props => props.theme.subtextColor}; + font-size: 11px; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: ${props => (props.showDatasetList ? '16px' : '4px')}; +`; + +const StyledDatasetSection = styled.div` + margin: 0 -32px 0 -16px; + padding: 0 32px 0 16px; + border-bottom: 1px solid ${props => props.theme.sidePanelBorderColor}; +`; + +export function AddDataButtonFactory() { + const AddDataButton = ({onClick, isInactive}) => ( + + ); + + return AddDataButton; +} + +DatasetSectionFactory.deps = [SourceDataCatalogFactory, AddDataButtonFactory]; + +function DatasetSectionFactory(SourceDataCatalog, AddDataButton) { + const DatasetSection = props => { + const { + datasets, + showDatasetTable, + updateTableColor, + showDeleteDataset, + removeDataset, + showDatasetList, + showAddDataModal, + defaultDataset + } = props; + const datasetCount = Object.keys(datasets).length; + return ( + + + Datasets ({datasetCount}) + + + {showDatasetList && ( + + )} + + ); + }; + + return DatasetSection; +} + +export default DatasetSectionFactory; diff --git a/src/components/side-panel/layer-panel/layer-config-group.js b/src/components/side-panel/layer-panel/layer-config-group.js index 435da6a6c2..2514c25f9f 100644 --- a/src/components/side-panel/layer-panel/layer-config-group.js +++ b/src/components/side-panel/layer-panel/layer-config-group.js @@ -105,7 +105,7 @@ LayerConfigGroupLabelFactory.deps = [InfoHelperFactory]; export function LayerConfigGroupLabelFactory(InfoHelper) { const StyledLayerConfigGroupLabel = styled.div` border-left: ${props => props.theme.layerConfigGroupLabelBorderLeft} solid - ${props => props.theme.labelColor}; + ${props => props.theme.layerConfigGroupLabelBorderColor || props.theme.labelColor}; line-height: 12px; margin-left: ${props => props.theme.layerConfigGroupLabelMargin}; padding-left: ${props => props.theme.layerConfigGroupLabelPadding}; diff --git a/src/components/side-panel/layer-panel/layer-list.d.ts b/src/components/side-panel/layer-panel/layer-list.d.ts new file mode 100644 index 0000000000..e79a01515c --- /dev/null +++ b/src/components/side-panel/layer-panel/layer-list.d.ts @@ -0,0 +1,19 @@ +import React from 'react'; +import {Layer, LayerClassesType} from 'layers'; +import {Datasets} from 'reducers/vis-state-updaters'; +import * as VisStateActions from 'actions/vis-state-actions'; +import * as UIStateActions from 'actions/ui-state-actions'; + +export type LayerListProps = { + datasets: Datasets; + layerClasses: LayerClassesType; + layers: Layer[]; + layerOrder: number[]; + uiStateActions: typeof UIStateActions; + visStateActions: typeof VisStateActions; + isSortable?: Boolean; +}; + +export default function LayerListFactory( + LayerPanel: React.Component +): React.Component; diff --git a/src/components/side-panel/layer-panel/layer-list.js b/src/components/side-panel/layer-panel/layer-list.js new file mode 100644 index 0000000000..506bd9694a --- /dev/null +++ b/src/components/side-panel/layer-panel/layer-list.js @@ -0,0 +1,174 @@ +// 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 React, {useCallback, useMemo, useState} from 'react'; +import {arrayMove} from 'utils/data-utils'; +import styled from 'styled-components'; +import classnames from 'classnames'; +import {SortableContainer, SortableElement} from 'react-sortable-hoc'; +import LayerPanelFactory from './layer-panel'; +// make sure the element is always visible while is being dragged +// item being dragged is appended in body, here to reset its global style +const SortableStyledItem = styled.div` + z-index: ${props => props.theme.dropdownWrapperZ + 1}; + &.sorting { + pointer-events: none; + } + &.sorting-layers .layer-panel__header { + background-color: ${props => props.theme.panelBackgroundHover}; + font-family: ${props => props.theme.fontFamily}; + font-weight: ${props => props.theme.fontWeight}; + font-size: ${props => props.theme.fontSize}; + line-height: ${props => props.theme.lineHeight}; + *, + *:before, + *:after { + box-sizing: border-box; + } + .layer__drag-handle { + opacity: 1; + color: ${props => props.theme.textColorHl}; + } + } +`; +LayerListFactory.deps = [LayerPanelFactory]; +function LayerListFactory(LayerPanel) { + // By wrapping layer panel using a sortable element we don't have to implement the drag and drop logic into the panel itself; + // Developers can provide any layer panel implementation and it will still be sortable + const SortableItem = SortableElement(({children, isSorting}) => { + return ( + + {children} + + ); + }); + const WrappedSortableContainer = SortableContainer(({children}) => { + return
{children}
; + }); + const LayerList = props => { + const { + layers, + datasets, + layerOrder, + uiStateActions, + visStateActions, + layerClasses, + isSortable = true + } = props; + const {toggleModal: openModal} = uiStateActions; + const [isSorting, setIsSorting] = useState(false); + const layerTypeOptions = useMemo( + () => + Object.keys(layerClasses).map(key => { + const layer = new layerClasses[key](); + return { + id: key, + label: layer.name, + icon: layer.layerIcon, + requireData: layer.requireData + }; + }), + [layerClasses] + ); + + const layerActions = { + layerColorUIChange: visStateActions.layerColorUIChange, + layerConfigChange: visStateActions.layerConfigChange, + layerVisualChannelConfigChange: visStateActions.layerVisualChannelConfigChange, + layerTypeChange: visStateActions.layerTypeChange, + layerVisConfigChange: visStateActions.layerVisConfigChange, + layerTextLabelChange: visStateActions.layerTextLabelChange, + removeLayer: visStateActions.removeLayer, + duplicateLayer: visStateActions.duplicateLayer + }; + + const panelProps = { + datasets, + openModal, + layerTypeOptions + }; + + const _handleSort = useCallback( + ({oldIndex, newIndex}) => { + visStateActions.reorderLayer(arrayMove(props.layerOrder, oldIndex, newIndex)); + setIsSorting(false); + }, + [props.layerOrder, visStateActions] + ); + const _onSortStart = useCallback(() => { + setIsSorting(true); + }, []); + const _updateBeforeSortStart = useCallback(() => { + ({index}) => { + const layerIdx = layerOrder[index]; + if (layers[layerIdx].config.isConfigActive) { + visStateActions.layerConfigChange(layers[layerIdx], {isConfigActive: false}); + } + }; + }, [layers, layerOrder, visStateActions]); + return isSortable ? ( + + {layerOrder.map( + (layerIdx, index) => + layers[layerIdx] && + !layers[layerIdx].config.hidden && ( + + + + ) + )} + + ) : ( + <> + {layerOrder.map( + (layerIdx, index) => + layers[layerIdx] && + !layers[layerIdx].config.hidden && ( + + ) + )} + + ); + }; + return LayerList; +} +export default LayerListFactory; diff --git a/src/components/side-panel/layer-panel/layer-panel-header.js b/src/components/side-panel/layer-panel/layer-panel-header.js index 6aba2c4713..4538478f34 100644 --- a/src/components/side-panel/layer-panel/layer-panel-header.js +++ b/src/components/side-panel/layer-panel/layer-panel-header.js @@ -58,6 +58,12 @@ const StyledLayerPanelHeader = styled(StyledPanelHeader)` .layer__remove-layer { opacity: 0; } + + .layer__drag-handle__placeholder { + height: 20px; + padding: 10px; + } + :hover { cursor: pointer; background-color: ${props => props.theme.panelBackgroundHover}; @@ -176,10 +182,12 @@ function LayerPanelHeaderFactory(LayerTitleSection, PanelHeaderAction) { onClick={toggleLayerConfigurator} > - {isDragNDropEnabled && ( + {isDragNDropEnabled ? ( + ) : ( +
)} { @@ -113,7 +114,7 @@ function LayerPanelFactory(LayerConfigurator, LayerPanelHeader) { }; render() { - const {layer, datasets, layerTypeOptions} = this.props; + const {layer, datasets, isDraggable, layerTypeOptions} = this.props; const {config} = layer; const {isConfigActive} = config; @@ -137,6 +138,7 @@ function LayerPanelFactory(LayerConfigurator, LayerPanelHeader) { onUpdateLayerLabel={this._updateLayerLabel} onRemoveLayer={this._removeLayer} onDuplicateLayer={this._duplicateLayer} + isDragNDropEnabled={isDraggable} /> {isConfigActive && ( + props.active + ? props.theme.layerPanelToggleOptionColorActive + : props.theme.layerPanelToggleOptionColor}; + :hover { + cursor: pointer; + color: ${props => props.theme.layerPanelToggleOptionColorActive}; + } +`; + +function ToggleOptionFactory() { + const ToggleOption = ({isActive, onClick, option}) => ( + + + + + + + + + ); + + return ToggleOption; +} + +const TOGGLE_OPTIONS = [ + { + id: 'list', + iconComponent: OrderByList, + value: 'list', + label: 'List' + }, + { + id: 'sort-by-dataset', + iconComponent: OrderByDataset, + value: 'sortByDataset', + label: 'Sort by dataset' + } +]; + +PanelViewListToggleFactory.deps = [ToggleOptionFactory]; + +function PanelViewListToggleFactory(ToggleOption) { + const PanelViewListToggle = props => { + const {layerPanelListViewMode, toggleLayerPanelListView} = props; + + const toggleListView = listView => toggleLayerPanelListView(listView); + const options = useMemo( + () => TOGGLE_OPTIONS.map(opt => ({...opt, isActive: layerPanelListViewMode === opt.value})), + [layerPanelListViewMode] + ); + + return ( + + + {options.map(opt => ( + toggleListView(opt.value)} + option={opt} + isActive={opt.isActive} + /> + ))} + + + ); + }; + + return PanelViewListToggle; +} + +export default PanelViewListToggleFactory; diff --git a/src/components/side-panel/panel-title.js b/src/components/side-panel/panel-title.js index d4dc9e1c9e..2a81a7b45b 100644 --- a/src/components/side-panel/panel-title.js +++ b/src/components/side-panel/panel-title.js @@ -20,7 +20,9 @@ import styled from 'styled-components'; -const PanelTitleFactory = () => styled.div` +const PanelTitleFactory = () => styled.div.attrs({ + className: 'panel-title' +})` color: ${props => props.theme.titleTextColor}; font-size: ${props => props.theme.sidePanelTitleFontsize}; line-height: ${props => props.theme.sidePanelTitleLineHeight}; diff --git a/src/constants/action-types.d.ts b/src/constants/action-types.d.ts index 66649d23e5..1dd2c461cf 100644 --- a/src/constants/action-types.d.ts +++ b/src/constants/action-types.d.ts @@ -25,7 +25,7 @@ export type ActionType = { REORDER_LAYER: string; SET_FILTER: string; SET_FILTER_ANIMATION_TIME: string; - SET_FILTER_ANIMATION_TIME_CONFIG: string; + SET_FILTER_ANIMATION_TIME_CONFIG: string; SET_FILTER_ANIMATION_WINDOW: string; SHOW_DATASET_TABLE: string; UPDATE_LAYER_BLENDING: string; @@ -92,6 +92,7 @@ export type ActionType = { ADD_NOTIFICATION: string; REMOVE_NOTIFICATION: string; SET_LOCALE: string; + TOGGLE_LAYER_PANEL_LIST_VIEW: string; // uiState > export image SET_EXPORT_IMAGE_SETTING: string; diff --git a/src/constants/action-types.js b/src/constants/action-types.js index 6afc0c8c15..0ef33f62b0 100644 --- a/src/constants/action-types.js +++ b/src/constants/action-types.js @@ -143,6 +143,7 @@ const ActionTypes = { ADD_NOTIFICATION: `${ACTION_PREFIX}ADD_NOTIFICATION`, REMOVE_NOTIFICATION: `${ACTION_PREFIX}REMOVE_NOTIFICATION`, SET_LOCALE: `${ACTION_PREFIX}SET_LOCALE`, + TOGGLE_LAYER_PANEL_LIST_VIEW: `${ACTION_PREFIX}TOGGLE_LAYER_PANEL_LIST_VIEW`, // uiState > export image SET_EXPORT_IMAGE_SETTING: `${ACTION_PREFIX}SET_EXPORT_IMAGE_SETTING`, diff --git a/src/constants/default-settings.d.ts b/src/constants/default-settings.d.ts new file mode 100644 index 0000000000..b8c9cd6d32 --- /dev/null +++ b/src/constants/default-settings.d.ts @@ -0,0 +1,390 @@ +import {Component} from 'react'; + +export const ALL_FIELD_TYPES: { + boolean: 'boolean'; + date: 'date'; + geojson: 'geojson'; + integer: 'integer'; + real: 'real'; + string: 'string'; + timestamp: 'timestamp'; + point: 'point'; +}; + +export const ANIMATION_WINDOW: { + free: 'free'; + incremental: 'incremental'; + point: 'point'; + interval: 'interval'; +}; + +export const FILTER_TYPES: { + range: 'range'; + select: 'select'; + input: 'input'; + timeRange: 'timeRange'; + multiSelect: 'multiSelect'; + polygon: 'polygon'; +}; + +export const DEFAULT_TIME_FORMAT: string; + +export const BASE_SPEED: number; +export const FPS: number; +export const SPEED_CONTROL_RANGE: [number, number]; +export const SPEED_CONTROL_STEP: number; + +export type SCALE_TYPES_DEF = { + ordinal: 'ordinal'; + quantile: 'quantile'; + quantize: 'quantize'; + linear: 'linear'; + sqrt: 'sqrt', + log: 'log'; + point: 'point'; +} + +export const SCALE_TYPES: SCALE_TYPES_DEF; + +export type SCALE_FUNC_TYPE = { + [key: keyof SCALE_TYPES_DEF]: () => number +} + +export const SCALE_FUNC: SCALE_FUNC_TYPE; + +export const SORT_ORDER: { + ASCENDING: 'ASCENDING'; + DESCENDING: 'DESCENDING'; + UNSORT: 'UNSORT'; +}; + +export type TABLE_OPTION_TYPE = { + SORT_ASC: 'SORT_ASC'; + SORT_DES: 'SORT_DES'; + UNSORT: 'UNSORT'; + PIN: 'PIN'; + UNPIN: 'UNPIN'; + COPY: 'COPY'; +}; + +export const TABLE_OPTION: TABLE_OPTION_TYPE; + +export type TABLE_OPTION = { + value: keyof TABLE_OPTION_TYPE; + display: string; + icon: Component; + condition: (props: any) => boolean; +}; + +export const TABLE_OPTION_LIST: TABLE_OPTION[]; + +export type FILED_TYPE_DISPLAY_TYPE = { + label: string; + color: string; +} +export const FILED_TYPE_DISPLAY: {[key: keyof ALL_FIELD_TYPES_DEF]: FILED_TYPE_DISPLAY_TYPE}; + +export const FIELD_COLORS: {default: string}; + +export const KEPLER_GL_NAME: string; +export const KEPLER_GL_VERSION: string; +export const KEPLER_GL_WEBSITE: string; + +export const EDITOR_MODES: { + READ_ONLY: 'READ_ONLY'; + DRAW_POLYGON: 'DRAW_POLYGON'; + DRAW_RECTANGLE: 'DRAW_RECTANGLE'; + EDIT: 'EDIT'; +}; + +export type LAYER_TYPES = { + point: 'point'; + arc: 'arc'; + line: 'line'; + grid: 'grid'; + hexagon: 'hexagon'; + geojson: 'geojson'; + cluster: 'cluster'; + icon: 'icon'; + heatmap: 'heatmap'; + hexagonId: 'hexagonId'; + '3D': '3D'; + trip: 'trip'; + s2: 's2'; +} + +export type EDITOR_AVAILABLE_LAYERS_DEF = { + point: 'point'; + arc: 'arc'; + line: 'line'; + hexagon: 'hexagon'; + hexagonId: 'hexagonId'; +} + +export const EDITOR_AVAILABLE_LAYERS: (keyof EDITOR_AVAILABLE_LAYERS_DEF)[]; + +export const GEOCODER_DATASET_NAME: string; +export const GEOCODER_LAYER_ID: string; +export const GEOCODER_GEO_OFFSET: number; +export const GEOCODER_ICON_COLOR: [number, number, number]; +export const GEOCODER_ICON_SIZE: number; + +export const DIMENSIONS: { + sidePanel: { + width: number; + margin: { + top: number; + left: number; + bottom: number; + right: number; + }, + headerHeight: number; + }; + mapControl: { + width: number; + padding: number; + mapLegend: { + pinned: { + bottom: number; + right: number; + } + } + } +}; + +export const THEME: { + light: 'light'; + dark: 'dark'; + base: 'base'; +}; + +export const DEFAULT_MAPBOX_API_URL: string; + +export const THROTTLE_NOTIFICATION_TIME: number; + +export const CHANNEL_SCALES: { + color: 'color'; + radius: 'radius'; + size: 'size'; + colorAggr: 'colorAggr'; + sizeAggr: 'sizeAggr'; +}; + +export const EXPORT_DATA_TYPE: { + CSV: string; +}; + +export type EXPORT_DATA_TYPE_OPTION = { + id: keyof EXPORT_DATA_TYPE_DEF; + label: string; + available: boolean +} +export const EXPORT_DATA_TYPE_OPTIONS: EXPORT_DATA_TYPE_OPTION[]; + +export const EXPORT_MAP_FORMATS: { + HTML: 'HTML'; + JSON: 'JSON'; +}; + +export const EXPORT_IMG_RATIOS: { + SCREEN: 'SCREEN'; + FOUR_BY_THREE: 'FOUR_BY_THREE'; + SIXTEEN_BY_NINE: 'SIXTEEN_BY_NINE'; + CUSTOM: 'CUSTOM'; +}; + +export type EXPORT_IMG_RATIO_OPTION = { + id: keyof EXPORT_IMG_RATIOS_TYPE; + label: string; + getSize: (screenW: number, screenH: number) => {width: number, height: number}; + hidden?: boolean; +} + +export const EXPORT_IMG_RATIO_OPTIONS: EXPORT_IMG_RATIO_OPTION[]; + +export const RESOLUTIONS: { + ONE_X: 'ONE_X'; + TWO_X: 'TWO_X'; +}; + +export type EXPORT_IMG_RESOLUTION_OPTION = EXPORT_IMG_RATIO_OPTION & { + available: boolean; + scale: number; +} +export const EXPORT_IMG_RESOLUTION_OPTIONS: EXPORT_IMG_RESOLUTION_OPTION[]; + +export const EXPORT_HTML_MAP_MODES: { + READ: 'READ'; + EDIT: 'EDIT'; +}; + +export type EXPORT_HTML_MAP_MODE_OPTION = { + id: keyof EXPORT_HTML_MAP_MODES_TYPE; + label: string; + available: boolean; + url: string; +}; +export const EXPORT_HTML_MAP_MODE_OPTIONS: EXPORT_HTML_MAP_MODE_OPTION[]; + +export type EXPORT_MAP_FORMAT_OPTION = { + id: keyof EXPORT_MAP_FORMATS_TYPE, + label: string; + available: string; +}; +export const EXPORT_MAP_FORMAT_OPTIONS: EXPORT_MAP_FORMAT_OPTION[]; + +export type MAP_THUMBNAIL_DIMENSION_TYPE = { + width: number; + height: number; +}; +export const MAP_THUMBNAIL_DIMENSION: MAP_THUMBNAIL_DIMENSION_TYPE; + +export const LOADING_METHODS: { + upload: 'upload'; + storage: 'storage'; +}; + +export type MAP_INFO_CHARACTER_TYPE = { + title: number; + description: number; +} +export const MAP_INFO_CHARACTER: MAP_INFO_CHARACTER_TYPE; + +export type FIELD_OPT = { + type: string; + scale: object; + format: object; +}; +export const FIELD_OPTS: { + string: FIELD_OPT, + real: FIELD_OPT, + timestamp: FIELD_OPT, + integer: FIELD_OPT, + boolean: FIELD_OPT, + date: FIELD_OPT, + geojson: FIELD_OPT +} + +export type DEFAULT_NOTIFICATION_TOPICS_TYPE = { + global: string; + field: string; +}; +export const DEFAULT_NOTIFICATION_TOPICS: DEFAULT_NOTIFICATION_TOPICS_TYPE; +export type SIDEBAR_PANELS_TYPE = { + id: string; + label: string; + iconComponent: Component; + onClick: () => void; +}; +export const SIDEBAR_PANELS: SIDEBAR_PANELS_TYPE[]; +export const PANELS: SIDEBAR_PANELS_TYPE[]; + +export type DEFAULT_LAYER_GROUP = { + slug: string; + filter: (value) => boolean; + defaultVisibility: boolean; +}; +export const DEFAULT_LAYER_GROUPS: DEFAULT_LAYER_GROUP[]; + +export const AGGREGATION_TYPES: { + count: string; + average: string; + maximum: string; + minimum: string; + median: string; + stdev: string; + sum: string; + variance: string; + mode: string; + countUnique: string; +}; + +export const DATASET_FORMATS: { + row: string; + geojson: string; + csv: string; + keplergl: string; +}; + +export const DEFAULT_MAP_STYLES: { + id: string; + label: string; + url: string; + icon: string; + layerGroup: DEFAULT_LAYER_GROUPS; +}[]; + +export type LAYER_BLENDING_TYPE = { + label: string; + blendFunc: string[]; + blendEquation: string | string[]; +} +export const LAYER_BLENDINGS: { + additive: LAYER_BLENDING_TYPE; + normal: LAYER_BLENDING_TYPE; + subtractive: LAYER_BLENDING_TYPE; +}; + +export type DEFAULT_NOTIFICATION_TYPES_DEF = { + info: string; + error: string; + warning: string; + success: string; +}; +export const DEFAULT_NOTIFICATION_TYPES: DEFAULT_NOTIFICATION_TYPES_DEF; + +export const TRIP_ARC_FIELDS: { + lat0: string; + lng0: string; + lat1: string; + lng1: string; +} + +export const TRIP_POINT_FIELDS: [string, string][]; + +export const GEOJSON_FIELDS: { + geojson: string[]; +} + +export const ICON_FIELDS: { + icon: string[]; +} + +export const MAP_CONTROLS: { + visibleLayers: 'visibleLayers'; + mapLegend: 'mapLegend'; + toggle3d: 'toggle3d'; + toggleGlobe: 'toggleGlobe'; + splitMap: 'splitMap'; + mapDraw: 'mapDraw'; + mapLocale: 'mapLocale'; +} + +export const DEFAULT_LAYER_COLOR: { + tripArc: string; + begintrip_lat: string; + dropoff_lat: string; + request_lat: string; +}; + +export const CHANNEL_SCALE_SUPPORTED_FIELDS: object; +export const CLOUDFRONT: string; +export const MAX_DEFAULT_TOOLTIPS: number; +export const NO_VALUE_COLOR: [number, number, number, number]; +export const MAX_GPU_FILTERS: number; +export const DEFAULT_TOOLTIP_FIELDS: {name: string}[]; +export const DEFAULT_NOTIFICATION_MESSAGE: string; +export const DEFAULT_UUID_COUNT: number; + +// modals +export const DATA_TABLE_ID: string; +export const ADD_DATA_ID: string; +export const DELETE_DATA_ID: string; +export const EXPORT_IMAGE_ID: string; +export const EXPORT_DATA_ID: string; +export const ADD_MAP_STYLE_ID: string; +export const EXPORT_MAP_ID: string; +export const SAVE_MAP_ID: string; +export const OVERWRITE_MAP_ID: string; +export const SHARE_MAP_ID: string; + diff --git a/src/constants/index.d.ts b/src/constants/index.d.ts new file mode 100644 index 0000000000..67d01e0e87 --- /dev/null +++ b/src/constants/index.d.ts @@ -0,0 +1,6 @@ +export {PLOT_TYPES} from '../utils/filter-utils'; + +export * from './action-types'; +export * from './default-settings'; +export * as KeyEvent from './keyevent'; + diff --git a/src/constants/keyevent.d.ts b/src/constants/keyevent.d.ts new file mode 100644 index 0000000000..4f20ae0f2d --- /dev/null +++ b/src/constants/keyevent.d.ts @@ -0,0 +1,8 @@ +export const DOM_VK_UP: string|number; +export const DOM_VK_DOWN: string|number; +export const DOM_VK_BACK_SPACE: string|number; +export const DOM_VK_RETURN: string|number; +export const DOM_VK_ENTER: string|number; +export const DOM_VK_ESCAPE: string|number; +export const DOM_VK_TAB: string|number; +export const DOM_VK_DELETE: string|number; diff --git a/src/index.d.ts b/src/index.d.ts new file mode 100644 index 0000000000..3c46ed49d1 --- /dev/null +++ b/src/index.d.ts @@ -0,0 +1,2 @@ +export * from './actions'; +export * from './components'; diff --git a/src/layers/trip-layer/trip-utils.d.ts b/src/layers/trip-layer/trip-utils.d.ts new file mode 100644 index 0000000000..4e9884d185 --- /dev/null +++ b/src/layers/trip-layer/trip-utils.d.ts @@ -0,0 +1,3 @@ +import {Field} from '../../reducers'; + +export const containValidTime: (timestamps: string[]) => Field | null; diff --git a/src/layers/trip-layer/trip-utils.js b/src/layers/trip-layer/trip-utils.js index 4fde96f6de..913ebc1e9a 100644 --- a/src/layers/trip-layer/trip-utils.js +++ b/src/layers/trip-layer/trip-utils.js @@ -57,7 +57,7 @@ export function containValidTime(timestamps) { const analyzedType = Analyzer.computeColMeta(formattedTimeStamps, [], {ignoredDataTypes})[0]; if (!analyzedType || analyzedType.category !== 'TIME') { - return false; + return null; } return analyzedType; } diff --git a/src/localization/formatted-message.d.ts b/src/localization/formatted-message.d.ts new file mode 100644 index 0000000000..81d7df828a --- /dev/null +++ b/src/localization/formatted-message.d.ts @@ -0,0 +1,15 @@ +import React from 'react'; + +declare type FormattedMessageType = { + id: string; + defaultMessage?: string; + defaultValue?: string; + values?: { + [key: string]: string | number; + }; + children?: () => React.ReactElement; +}; + +declare const EnhancedFormattedMessage: React.FC; + +export default EnhancedFormattedMessage; diff --git a/src/localization/formatted-message.js b/src/localization/formatted-message.js index 7b3c67ccf6..dbba09d880 100644 --- a/src/localization/formatted-message.js +++ b/src/localization/formatted-message.js @@ -21,13 +21,12 @@ import React from 'react'; import {FormattedMessage} from 'react-intl'; -const ExhancedFormattedMessage = props => ( +const EnhancedFormattedMessage = props => ( ); -export default ExhancedFormattedMessage; +export default EnhancedFormattedMessage; diff --git a/src/localization/index.d.ts b/src/localization/index.d.ts new file mode 100644 index 0000000000..a9cb688004 --- /dev/null +++ b/src/localization/index.d.ts @@ -0,0 +1,3 @@ +export {LOCALE_CODES, LOCALES} from './locales'; +export {default as FormattedMessage} from './formatted-message'; +export {messages} from './messages'; diff --git a/src/localization/index.js b/src/localization/index.js index ab24253c09..25749d184b 100644 --- a/src/localization/index.js +++ b/src/localization/index.js @@ -18,20 +18,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import en from './en'; -import {flattenMessages} from 'utils/locale-utils'; -import {LOCALE_CODES} from './locales'; - -const enFlat = flattenMessages(en); - -export const messages = Object.keys(LOCALE_CODES).reduce( - (acc, key) => ({ - ...acc, - [key]: key === 'en' ? enFlat : {...enFlat, ...flattenMessages(require(`./${key}`).default)} - }), - {} -); - +export {messages} from './messages'; export {LOCALE_CODES, LOCALES} from './locales'; - export {default as FormattedMessage} from './formatted-message'; diff --git a/src/localization/locales.d.ts b/src/localization/locales.d.ts new file mode 100644 index 0000000000..4928c2dd73 --- /dev/null +++ b/src/localization/locales.d.ts @@ -0,0 +1,12 @@ +export declare const LOCALES: { + en: string; + fi: string; + pt: string; + es: string; + ca: string; + ja: string; + cn: string; + ru: string; +}; + +export declare const LOCALE_CODES: {[key: string]: string}; diff --git a/src/localization/messages.d.ts b/src/localization/messages.d.ts new file mode 100644 index 0000000000..b0c4f18367 --- /dev/null +++ b/src/localization/messages.d.ts @@ -0,0 +1,5 @@ +export declare const messages: { + [key: string]: string; +}; + +export default messages; diff --git a/src/localization/messages.js b/src/localization/messages.js new file mode 100644 index 0000000000..93f2bc8e9e --- /dev/null +++ b/src/localization/messages.js @@ -0,0 +1,18 @@ +import en from './translations/en'; +import {flattenMessages} from '../utils/locale-utils'; +import {LOCALE_CODES} from './locales'; + +const enFlat = flattenMessages(en); + +export const messages = Object.keys(LOCALE_CODES).reduce( + (acc, key) => ({ + ...acc, + [key]: + key === 'en' + ? enFlat + : {...enFlat, ...flattenMessages(require(`./translations/${key}.js`).default)} + }), + {} +); + +export default messages; diff --git a/src/localization/ca.js b/src/localization/translations/ca.js similarity index 99% rename from src/localization/ca.js rename to src/localization/translations/ca.js index f740bc6a0a..0724b52aec 100644 --- a/src/localization/ca.js +++ b/src/localization/translations/ca.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/cn.js b/src/localization/translations/cn.js similarity index 99% rename from src/localization/cn.js rename to src/localization/translations/cn.js index 14226e5936..922c393ca6 100644 --- a/src/localization/cn.js +++ b/src/localization/translations/cn.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/en.js b/src/localization/translations/en.js similarity index 99% rename from src/localization/en.js rename to src/localization/translations/en.js index 9b85c44026..7a78bb0db6 100644 --- a/src/localization/en.js +++ b/src/localization/translations/en.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/es.js b/src/localization/translations/es.js similarity index 99% rename from src/localization/es.js rename to src/localization/translations/es.js index 601cb33eb2..663e48e547 100644 --- a/src/localization/es.js +++ b/src/localization/translations/es.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/fi.js b/src/localization/translations/fi.js similarity index 99% rename from src/localization/fi.js rename to src/localization/translations/fi.js index d7f73805d7..3fced0d055 100644 --- a/src/localization/fi.js +++ b/src/localization/translations/fi.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/ja.js b/src/localization/translations/ja.js similarity index 99% rename from src/localization/ja.js rename to src/localization/translations/ja.js index 5d23d68d2f..04a33aefc3 100644 --- a/src/localization/ja.js +++ b/src/localization/translations/ja.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/pt.js b/src/localization/translations/pt.js similarity index 99% rename from src/localization/pt.js rename to src/localization/translations/pt.js index e39218ec24..83b48885e6 100644 --- a/src/localization/pt.js +++ b/src/localization/translations/pt.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/localization/ru.js b/src/localization/translations/ru.js similarity index 99% rename from src/localization/ru.js rename to src/localization/translations/ru.js index 9f4677825e..1028107e57 100644 --- a/src/localization/ru.js +++ b/src/localization/translations/ru.js @@ -18,7 +18,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -import {LOCALES} from './locales'; +import {LOCALES} from '../locales'; export default { property: { diff --git a/src/middleware/index.d.ts b/src/middleware/index.d.ts new file mode 100644 index 0000000000..a15dc69249 --- /dev/null +++ b/src/middleware/index.d.ts @@ -0,0 +1,3 @@ +import {Middleware} from 'redux'; + +export const enhanceReduxMiddleware: (middlewares: Middleware[]) => Middleware[]; diff --git a/src/processors/data-processor.js b/src/processors/data-processor.js index e5bf3a9e97..ad21de4e0d 100644 --- a/src/processors/data-processor.js +++ b/src/processors/data-processor.js @@ -439,9 +439,10 @@ export function processRowObject(rawData) { return null; } else if (!rawData.length) { // data is empty - return { - fields: [], rows: [] - } + return { + fields: [], + rows: [] + }; } const keys = Object.keys(rawData[0]); diff --git a/src/processors/index.d.ts b/src/processors/index.d.ts new file mode 100644 index 0000000000..cf01a97de1 --- /dev/null +++ b/src/processors/index.d.ts @@ -0,0 +1,2 @@ +export * from './data-processor'; +export * from './file-handler'; diff --git a/src/reducers/ui-state-updaters.d.ts b/src/reducers/ui-state-updaters.d.ts index b7aad4a070..34be98db64 100644 --- a/src/reducers/ui-state-updaters.d.ts +++ b/src/reducers/ui-state-updaters.d.ts @@ -71,6 +71,8 @@ export type Notifications = { export type Locale = string; +export type LayerPanelListView = 'list' | 'sortByDataset' + export type UiState = { readOnly: boolean; activeSidePanel: string; @@ -91,6 +93,7 @@ export type UiState = { loadFiles: LoadFiles; // Locale of the UI locale: Locale; + layerPanelListView: LayerPanelListView; }; export const DEFAULT_MAP_CONTROLS: MapControls; @@ -186,6 +189,10 @@ export function setLocaleUpdater( state: UiState, action: UiStateActions.SetLocaleUpdaterAction ): UiState; +export function toggleLayerPanelListViewUpdater( + state: UiState, + action: UiStateActions.ToggleLayerPanelListViewAction +): UiState; export function loadFilesUpdater(state: UiState, action: LoadFilesUpdaterAction): UiState; export function loadFilesErrUpdater(state: UiState, action: LoadFilesErrUpdaterAction): UiState; diff --git a/src/reducers/ui-state-updaters.js b/src/reducers/ui-state-updaters.js index be34a69797..ff0c97ad59 100644 --- a/src/reducers/ui-state-updaters.js +++ b/src/reducers/ui-state-updaters.js @@ -250,7 +250,8 @@ export const INITIAL_UI_STATE = { // load files loadFiles: DEFAULT_LOAD_FILES, // Locale of the UI - locale: LOCALE_CODES.en + locale: LOCALE_CODES.en, + layerPanelListView: 'list' }; /* Updaters */ @@ -764,3 +765,22 @@ export const setLocaleUpdater = (state, {payload: {locale}}) => ({ ...state, locale }); + +/** + * Toggle layer panel list view + * @memberof uiStateUpdaters + * @param state `uiState` + * @param action + * @param action.payload layer panel listView value. Can be 'list' or 'sortByDataset' + * @returns nextState + * @type {typeof import('./ui-state-updaters').toggleLayerPanelListViewUpdater} + * @public + */ +export const toggleLayerPanelListViewUpdater = (state, {payload: listView}) => { + return listView === state.layerPanelListView + ? state + : { + ...state, + layerPanelListView: listView + }; +}; diff --git a/src/reducers/ui-state.js b/src/reducers/ui-state.js index d9fd68d40d..f30b22e7d3 100644 --- a/src/reducers/ui-state.js +++ b/src/reducers/ui-state.js @@ -58,7 +58,8 @@ const actionHandler = { [ActionTypes.TOGGLE_SPLIT_MAP]: uiStateUpdaters.toggleSplitMapUpdater, [ActionTypes.SHOW_DATASET_TABLE]: uiStateUpdaters.showDatasetTableUpdater, - [ActionTypes.SET_LOCALE]: uiStateUpdaters.setLocaleUpdater + [ActionTypes.SET_LOCALE]: uiStateUpdaters.setLocaleUpdater, + [ActionTypes.TOGGLE_LAYER_PANEL_LIST_VIEW]: uiStateUpdaters.toggleLayerPanelListViewUpdater }; /* Reducer */ diff --git a/src/reducers/vis-state-updaters.js b/src/reducers/vis-state-updaters.js index 6f3efca80f..655f364b31 100644 --- a/src/reducers/vis-state-updaters.js +++ b/src/reducers/vis-state-updaters.js @@ -56,7 +56,11 @@ import { } from 'utils/filter-utils'; import {assignGpuChannel, setFilterGpuMode} from 'utils/gpu-filter-utils'; import {createNewDataEntry} from 'utils/dataset-utils'; -import {sortDatasetByColumn, copyTableAndUpdate} from 'utils/table-utils/kepler-table'; +import { + pinTableColumns, + sortDatasetByColumn, + copyTableAndUpdate +} from 'utils/table-utils/kepler-table'; import {set, toArray, arrayInsert, generateHashId} from 'utils/utils'; import {calculateLayerData, findDefaultLayer} from 'utils/layer-utils'; @@ -87,7 +91,7 @@ import {pick_, merge_, swap_} from './composer-helpers'; import {processFileContent} from 'actions/vis-state-actions'; import KeplerGLSchema from 'schemas'; -import {isRGBColor} from 'utils/color-utils'; +import {isRgbColor} from 'utils/color-utils'; // type imports /** @typedef {import('./vis-state-updaters').Field} Field */ @@ -940,8 +944,8 @@ export const addLayerUpdater = (state, action) => { newLayer = result.layer; newLayerData = result.layerData; } else { - // create an empty layer with the first available dataset - const defaultDataset = Object.keys(state.datasets)[0]; + // create an empty layer with a specific dataset or a default one + const defaultDataset = action.datasetId ?? Object.keys(state.datasets)[0]; newLayer = new Layer({ isVisible: true, isConfigActive: true, @@ -1141,7 +1145,7 @@ export const updateTableColorUpdater = (state, action) => { const {dataId, newColor} = action; const {datasets} = state; - if (isRGBColor(newColor)) { + if (isRgbColor(newColor)) { const existing = datasets[dataId]; existing.updateTableColor(newColor); @@ -2066,20 +2070,9 @@ export function pinTableColumnUpdater(state, {dataId, column}) { if (!dataset) { return state; } - const field = dataset.fields.find(f => f.name === column); - if (!field) { - return state; - } - - let pinnedColumns; - if (Array.isArray(dataset.pinnedColumns) && dataset.pinnedColumns.includes(field.name)) { - // unpin it - pinnedColumns = dataset.pinnedColumns.filter(co => co !== field.name); - } else { - pinnedColumns = (dataset.pinnedColumns || []).concat(field.name); - } + const newDataset = pinTableColumns(dataset, column); - return set(['datasets', dataId, 'pinnedColumns'], pinnedColumns, state); + return set(['datasets', dataId], newDataset, state); } /** diff --git a/src/styles/base.d.ts b/src/styles/base.d.ts new file mode 100644 index 0000000000..7e3f4b2381 --- /dev/null +++ b/src/styles/base.d.ts @@ -0,0 +1,5 @@ +export const transition: string; +export const themeLT: object; +export const theme: object; +export const themeBS: object; +export const inputBoxShadowActiveLT: string; diff --git a/src/styles/base.js b/src/styles/base.js index 6cd239f869..1a2c930ccd 100644 --- a/src/styles/base.js +++ b/src/styles/base.js @@ -273,6 +273,9 @@ export const layerTypeIconSizeL = 50; export const layerTypeIconPdL = 12; export const layerTypeIconSizeSM = 28; +export const layerPanelToggleOptionColor = '#6A7485'; +export const layerPanelToggleOptionColorActive = '#F0F0F0'; + // Sidepanel divider export const sidepanelDividerBorder = '1px'; export const sidepanelDividerMargin = 12; @@ -1311,6 +1314,9 @@ export const theme = { layerTypeIconPdL, layerTypeIconSizeSM, + layerPanelToggleOptionColor, + layerPanelToggleOptionColorActive, + // Text fontFamily, fontWeight, diff --git a/src/styles/index.d.ts b/src/styles/index.d.ts new file mode 100644 index 0000000000..cb5ddffa82 --- /dev/null +++ b/src/styles/index.d.ts @@ -0,0 +1,2 @@ +export * from './media-breakpoints'; +export * from './base'; diff --git a/src/styles/media-breakpoints.d.ts b/src/styles/media-breakpoints.d.ts new file mode 100644 index 0000000000..c467dec5c8 --- /dev/null +++ b/src/styles/media-breakpoints.d.ts @@ -0,0 +1,14 @@ +export type breakPointValuesType = { + palm: number; + desk: number; +} + +export const breakPointValues: breakPointValuesType; + +export type mediaType = { + palm: (...args: any) => string; + portable: (...args: any) => string; + desk: (...args: any) => string; +} + +export const media: mediaType; diff --git a/src/tasks/index.d.ts b/src/tasks/index.d.ts new file mode 100644 index 0000000000..ef08e25337 --- /dev/null +++ b/src/tasks/index.d.ts @@ -0,0 +1 @@ +module.exports = require('./dist/constants'); diff --git a/src/templates/index.d.ts b/src/templates/index.d.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/utils/color-utils.d.ts b/src/utils/color-utils.d.ts new file mode 100644 index 0000000000..1d13a8a33e --- /dev/null +++ b/src/utils/color-utils.d.ts @@ -0,0 +1,9 @@ +import {ColorRange} from '../layers/layer-factory'; + +export const hexToRgb: (hex: string) => [number, number, number]; +export const createLinearGradient: (string: direction, colors: number[]) => string; +export const reverseColorRange: (reversed: boolean, colorRange: ColorRange) => object; +export const rgbToHex: (colors: [number, number, number]) => string; + +export const getColorGroupByName: (ColorRange) => ColorRange; +export function isRgbColor(color: unknown): color is RGBColor; diff --git a/src/utils/color-utils.js b/src/utils/color-utils.js index e6a2518c8e..c15a603fda 100644 --- a/src/utils/color-utils.js +++ b/src/utils/color-utils.js @@ -106,11 +106,11 @@ export function createLinearGradient(direction, colors) { } /** - * Checking if color is RGB - * @param {*} color - * @returns boolean + * Whether color is rgb + * @type {typeof import('./color-utils').isRgbColor} + * @returns */ -export function isRGBColor(color) { +export function isRgbColor(color) { if ( color && Array.isArray(color) && diff --git a/src/utils/export-utils.d.ts b/src/utils/export-utils.d.ts new file mode 100644 index 0000000000..fa4255d1d9 --- /dev/null +++ b/src/utils/export-utils.d.ts @@ -0,0 +1,16 @@ +import {ExportImage} from 'reducers/ui-state-updaters'; + +export const downloadFile: (fileBlob: Blob, fileName: string) => void; +export const exportJson: (state: any, options: object) => void; +export const exportHtml: (state: any, options: object) => void; +export const exportData: (state: any, options: object) => void; +export const exportMap: (state: any, options?: object) => void; +export const exportImage: (uiStateExportImage: ExportImage, fileName?: string) => void; +export const convertToPng: (sourceElem: HTMLElement, options: object) => Promise; +export const getScaleFromImageSize: ( + imageW: number, + imageH: number, + mapW: number, + mapH: number +) => number; +export const dataURItoBlob: (dataURI: string) => Blob; diff --git a/src/utils/export-utils.js b/src/utils/export-utils.js index d9983ef568..010647514f 100644 --- a/src/utils/export-utils.js +++ b/src/utils/export-utils.js @@ -135,8 +135,13 @@ export function downloadFile(fileBlob, fileName) { } } -export function exportImage(state, filename = DEFAULT_IMAGE_NAME) { - const {imageDataUri} = state.uiState.exportImage; +/** + * Whether color is rgb + * @type {typeof import('./export-utils').exportImage} + * @returns + */ +export function exportImage(uiStateExportImage, filename = DEFAULT_IMAGE_NAME) { + const {imageDataUri} = uiStateExportImage; if (imageDataUri) { const file = dataURItoBlob(imageDataUri); downloadFile(file, filename); @@ -190,10 +195,10 @@ export function exportHtml(state, options) { downloadFile(fileBlob, state.appName ? `${state.appName}.html` : DEFAULT_HTML_NAME); } -export function exportData(state, option) { +export function exportData(state, options) { const {visState, appName} = state; const {datasets} = visState; - const {selectedDataset, dataType, filtered} = option; + const {selectedDataset, dataType, filtered} = options; // get the selected data const filename = appName ? appName : DEFAULT_DATA_NAME; const selectedDatasets = datasets[selectedDataset] @@ -226,10 +231,10 @@ export function exportData(state, option) { }); } -export function exportMap(state, option) { +export function exportMap(state, options) { const {imageDataUri} = state.uiState.exportImage; const thumbnail = imageDataUri ? dataURItoBlob(imageDataUri) : null; - const mapToSave = getMapJSON(state, option); + const mapToSave = getMapJSON(state, options); return { map: mapToSave, diff --git a/src/utils/index.d.ts b/src/utils/index.d.ts index 932a43e115..4ccd92b1bb 100644 --- a/src/utils/index.d.ts +++ b/src/utils/index.d.ts @@ -1,5 +1,14 @@ - -export {maybeToDate, roundValToStep} from './data-utils'; export {updateAllLayerDomainData} from '../reducers/vis-state-updaters'; -// export {getHexFields} from '../layers/h3-hexagon-layer/h3-utils'; -export {default as KeplerTable, Field, findPointFieldPairs, copyTableAndUpdate} from './table-utils/kepler-table'; \ No newline at end of file +export {default as KeplerTable, Field, findPointFieldPairs, copyTableAndUpdate} from './table-utils/kepler-table'; +export {downloadFile} from './export-utils'; +export {containValidTime} from '../layers/trip-layer/trip-utils'; +export {validateLayersByDatasets} from '../reducers/vis-state-merger'; +export * from './color-utils'; +export * from './data-scale-utils'; +export * from './data-utils'; +export * from './dataset-utils'; +export * from './filter-utils'; +export * from './gpu-filter-utils'; +export * from './interaction-utils'; +export * from './layer-utils'; +export * from './observe-dimensions'; diff --git a/src/utils/index.js b/src/utils/index.js index c40f7b68c7..3429baf147 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -47,5 +47,9 @@ export {updateAllLayerDomainData} from '../reducers/vis-state-updaters'; export {getHexFields} from '../layers/h3-hexagon-layer/h3-utils'; export {containValidTime} from '../layers/trip-layer/trip-utils'; -export {KeplerTable, findPointFieldPairs, copyTableAndUpdate} from './table-utils/kepler-table'; +export { + default as KeplerTable, + findPointFieldPairs, + copyTableAndUpdate +} from './table-utils/kepler-table'; export {createDataContainer, createIndexedDataContainer} from './table-utils/data-container-utils'; diff --git a/src/utils/interaction-utils.js b/src/utils/interaction-utils.js index c3f94da67b..2be3bab458 100644 --- a/src/utils/interaction-utils.js +++ b/src/utils/interaction-utils.js @@ -90,6 +90,7 @@ export function findFieldsToShow({fields, id, maxDefaultTooltips}) { // first find default tooltip fields for trips const fieldsToShow = DEFAULT_TOOLTIP_FIELDS.reduce((prev, curr) => { if (fields.find(({name}) => curr.name === name)) { + // @ts-ignore prev.push(curr); } return prev; diff --git a/src/utils/table-utils/kepler-table.d.ts b/src/utils/table-utils/kepler-table.d.ts index 74d496affc..f88286780d 100644 --- a/src/utils/table-utils/kepler-table.d.ts +++ b/src/utils/table-utils/kepler-table.d.ts @@ -69,7 +69,7 @@ export class KeplerTable { supportedFilterTypes?: ProtoDataset['supportedFilterTypes']; }); type?: string; - label?: string; + label: string; color: RGBColor; // fields and data @@ -95,22 +95,33 @@ export class KeplerTable { sortOrder?: number[] | null; pinnedColumns?: string[]; - supportedFilterTypes?: string[]; + supportedFilterTypes: string[] | undefined; // table-injected metadata - metadata?: object; + metadata: object; // methods getColumnField(columnName: string): Field | undefined; getColumnFieldIdx(columnName: string): number; - getColumnDomain(columnName: string): any[]; - + getColumnFilterDomain(field: Field): FieldDomain; + getColumnLayerDomain(field: Field, scaleType: string): number[] | string[] | [number, number]; getValue(columnName: string, rowIdx: number): any; updateColumnField(fieldIdx: number, newField: Field): void; updateTableColor(newColor: RGBColor): void; getColumnFilterProps(fieldName: string): Field['filterProps'] | null | undefined; filterTable(filters: Filter[], layers: Layer[], opt?: FilterDatasetOpt): KeplerTable; filterTableCPU(filters: Filter[], layers: Layer[]): KeplerTable; + + // private methods + _assetField(fieldName: string, condition: any): void; } +export function copyTable(original: T): T; +export function copyTableAndUpdate(original: T, options: Partial): T; +export function getFieldValueAccessor< + F extends { + type: Field['type']; + format: Field['format']; + } +>(f: F, i: number): FieldValueAccessor; +export function pinTableColumns(dataset: KeplerTable, column: string): KeplerTable; export default KeplerTable; -export function copyTableAndUpdate(original: KeplerTable, options: {}) \ No newline at end of file diff --git a/src/utils/table-utils/kepler-table.js b/src/utils/table-utils/kepler-table.js index beea541906..fab017cd8c 100644 --- a/src/utils/table-utils/kepler-table.js +++ b/src/utils/table-utils/kepler-table.js @@ -445,7 +445,7 @@ export function sortDatasetByColumn(dataset, column, mode) { return dataset; } - const sortBy = SORT_ORDER[mode] || SORT_ORDER.ASCENDING; + const sortBy = SORT_ORDER[mode || ''] || SORT_ORDER.ASCENDING; if (sortBy === SORT_ORDER.UNSORT) { dataset.sortColumn = {}; @@ -469,14 +469,36 @@ export function sortDatasetByColumn(dataset, column, mode) { return dataset; } +/** + * @type {typeof import('./kepler-table').pinTableColumns} + */ +export function pinTableColumns(dataset, column) { + const field = dataset.getColumnField(column); + if (!field) { + return dataset; + } + + let pinnedColumns; + if (Array.isArray(dataset.pinnedColumns) && dataset.pinnedColumns.includes(field.name)) { + // unpin it + pinnedColumns = dataset.pinnedColumns.filter(co => co !== field.name); + } else { + pinnedColumns = (dataset.pinnedColumns || []).concat(field.name); + } + + // @ts-ignore + return copyTableAndUpdate(dataset, {pinnedColumns}); +} +/** + * + * @type {typeof import('./kepler-table').copyTable} + */ export function copyTable(original) { return Object.assign(Object.create(Object.getPrototypeOf(original)), original); } /** * @type {typeof import('./kepler-table').copyTableAndUpdate} - * @param {KeplerTable} original - * @param {*} options * @returns */ export function copyTableAndUpdate(original, options = {}) { diff --git a/src/utils/utils.js b/src/utils/utils.js index 16043b0e5c..d113f38615 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -200,3 +200,17 @@ export function arrayInsert(arr, index, val) { export function isTest() { return process?.env?.NODE_ENV === 'test'; } + +/** + * Filters an object by an arbitrary predicate + * Returns a new object containing all elements that match the predicate + * @param {Object} obj Object to be filtered + * @param {Function} predicate Predicate by which the object will be filtered + * @returns {Object} + */ +export function filterObjectByPredicate(obj, predicate) { + return Object.entries(obj).reduce( + (acc, entry) => (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/side-panel/index.js b/test/browser/components/side-panel/index.js index 351a96bcba..2aeb7a3288 100644 --- a/test/browser/components/side-panel/index.js +++ b/test/browser/components/side-panel/index.js @@ -24,3 +24,5 @@ import './side-panel-test'; import './layer-panel-header-test'; import './filter-manager-test'; import './layer-configurator-test'; +import './layer-list-test'; +import './layer-manager-test'; diff --git a/test/browser/components/side-panel/layer-list-test.js b/test/browser/components/side-panel/layer-list-test.js new file mode 100644 index 0000000000..94b811d080 --- /dev/null +++ b/test/browser/components/side-panel/layer-list-test.js @@ -0,0 +1,103 @@ +// 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 React from 'react'; +import test from 'tape'; + +import {StateWMultiH3Layers} from 'test/helpers/mock-state'; + +import * as VisStateActions from 'actions/vis-state-actions'; +import * as UIStateActions from 'actions/ui-state-actions'; + +import {appInjector} from 'components/container'; +import {IntlWrapper, mountWithTheme} from 'test/helpers/component-utils'; + +import LayerListFactory from 'components/side-panel/layer-panel/layer-list'; + +const LayerList = appInjector.get(LayerListFactory); + +const defaultProps = { + datasets: StateWMultiH3Layers.visState.datasets, + layerClasses: StateWMultiH3Layers.visState.layerClasses, + layerOrder: StateWMultiH3Layers.visState.layerOrder, + layers: StateWMultiH3Layers.visState.layers, + uiStateActions: UIStateActions, + visStateActions: VisStateActions +}; + +test('Components -> SidePanel -> LayerPanel -> LayerList -> render sortable list', t => { + let wrapper; + t.doesNotThrow(() => { + wrapper = mountWithTheme( + + + + ); + }, 'LayerList should render'); + + t.equal(wrapper.find('.sortable-layer-items').length, 6, 'should render 6 sortable items'); + + const titles = []; + const expectedTitles = [ + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 2', + 'H3 Hexagon 2', + 'H3 Hexagon 2' + ]; + wrapper.find('.layer__title__editor').forEach(item => titles.push(item.getDOMNode().value)); + t.deepEqual(titles, expectedTitles, 'should render panels in correct order'); + + const layers = wrapper.find('.layer-panel'); + t.equal(layers.length, 6, 'should render 6 layer panels'); + + t.end(); +}); + +test('Components -> SidePanel -> LayerPanel -> LayerList -> render non-sortable list', t => { + let wrapper; + t.doesNotThrow(() => { + wrapper = mountWithTheme( + + + + ); + }, 'LayerList should render'); + + t.equal(wrapper.find('.sortable-layer-items').length, 0, 'should not render sortable items'); + + const titles = []; + const expectedTitles = [ + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 2', + 'H3 Hexagon 2', + 'H3 Hexagon 2' + ]; + wrapper.find('.layer__title__editor').forEach(item => titles.push(item.getDOMNode().value)); + t.deepEqual(titles, expectedTitles, 'should render panels in correct order'); + + const layers = wrapper.find('.layer-panel'); + t.equal(layers.length, 6, 'should render 6 layer panels'); + + t.end(); +}); diff --git a/test/browser/components/side-panel/layer-manager-test.js b/test/browser/components/side-panel/layer-manager-test.js new file mode 100644 index 0000000000..dcb8bd4074 --- /dev/null +++ b/test/browser/components/side-panel/layer-manager-test.js @@ -0,0 +1,147 @@ +// 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 React from 'react'; +import test from 'tape'; + +import { + LayerManagerFactory, + LayerListFactory, + DatasetLayerGroupFactory, + DatasetSectionFactory, + PanelViewListToggleFactory, + PanelTitleFactory, + AddLayerButtonFactory +} from 'components'; + +import {appInjector} from 'components/container'; +import {mountWithTheme, IntlWrapper} from 'test/helpers/component-utils'; + +import * as VisStateActions from 'actions/vis-state-actions'; +import * as UIStateActions from 'actions/ui-state-actions'; + +import {StateWMultiH3Layers} from 'test/helpers/mock-state'; + +import {Layers} from 'components/common/icons'; + +const LayerManager = appInjector.get(LayerManagerFactory); +const LayerList = appInjector.get(LayerListFactory); +const DatasetLayerGroup = appInjector.get(DatasetLayerGroupFactory); +const DatasetSection = appInjector.get(DatasetSectionFactory); +const PanelViewListToggle = appInjector.get(PanelViewListToggleFactory); +const PanelTitle = appInjector.get(PanelTitleFactory); +const AddLayerButton = appInjector.get(AddLayerButtonFactory); + +const nop = () => {}; + +const defaultProps = { + datasets: StateWMultiH3Layers.visState.datasets, + layers: StateWMultiH3Layers.visState.layers, + layerOrder: StateWMultiH3Layers.visState.layerOrder, + layerClasses: StateWMultiH3Layers.visState.layerClasses, + intl: {}, + showAddDataModal: nop, + updateTableColor: nop, + showDatasetTable: nop, + removeDataset: nop, + panelMetadata: { + id: 'layer', + label: 'sidebar.panels.layer', + iconComponent: Layers, + onClick: nop, + component: LayerManager + }, + layerPanelListView: 'list', + uiStateActions: UIStateActions, + visStateActions: VisStateActions, + layerBlending: 'normal' +}; + +test('Components -> LayerManager -> render -> list view', t => { + // mount + let wrapper; + t.doesNotThrow(() => { + wrapper = mountWithTheme( + + + + ); + }, 'LayerManager should not fail'); + + t.ok(wrapper.find(DatasetLayerGroup).length === 0, 'should not render DatasetLayerGroup'); + t.ok(wrapper.find(LayerList).length === 1, 'should render LayerList'); + t.ok(wrapper.find(AddLayerButton).length === 1, 'should render AddLayerButton'); + t.ok(wrapper.find(DatasetSection).length === 1, 'should render DatasetSection'); + t.ok(wrapper.find(PanelViewListToggle).length === 1, 'should render PanelViewListToggle'); + t.ok(wrapper.find(PanelTitle).length === 1, 'should render PanelTitle'); + + const titles = []; + const expectedTitles = [ + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 2', + 'H3 Hexagon 2', + 'H3 Hexagon 2' + ]; + wrapper.find('.layer__title__editor').forEach(item => titles.push(item.getDOMNode().value)); + t.deepEqual(titles, expectedTitles, 'should render panels in correct order'); + + const layers = wrapper.find('.layer-panel'); + t.equal(layers.length, 6, 'should render 6 layer panels'); + + t.end(); +}); + +test('Components -> LayerManager -> render -> order by dataset view', t => { + // mount + let wrapper; + t.doesNotThrow(() => { + wrapper = mountWithTheme( + + + + ); + }, 'LayerManager should not fail'); + + t.ok(wrapper.find(DatasetLayerGroup).length === 1, 'should render DatasetLayerGroup'); + t.ok(wrapper.find(LayerList).length === 1, 'should render LayerList'); + t.ok(wrapper.find(AddLayerButton).length === 1, 'should render AddLayerButton'); + t.ok(wrapper.find(DatasetSection).length === 1, 'should render DatasetSection'); + t.ok(wrapper.find(PanelViewListToggle).length === 1, 'should render PanelViewListToggle'); + t.ok(wrapper.find(PanelTitle).length === 1, 'should render PanelTitle'); + + const titles = []; + const expectedTitles = [ + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 1', + 'H3 Hexagon 2', + 'H3 Hexagon 2', + 'H3 Hexagon 2' + ]; + wrapper.find('.layer__title__editor').forEach(item => titles.push(item.getDOMNode().value)); + t.deepEqual(titles, expectedTitles, 'should render panels in correct order'); + + const layers = wrapper.find('.layer-panel'); + t.equal(layers.length, 6, 'should render 6 layer panels'); + + 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..560ea6101d 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); @@ -274,6 +275,57 @@ function mockStateWithH3Layer() { ]); return prepareState; } + +function mockStateWithMultipleH3Layers() { + const initialState = cloneDeep(InitialState); + + const prepareState = applyActions(keplerGlReducer, initialState, [ + { + action: addDataToMap, + payload: [ + { + datasets: { + info: {id: csvDataId}, + data: processCsvData(testLayerData) + }, + config: { + version: 'v1', + config: { + visState: { + layers: [ + { + id: 'h3-layer-1', + type: 'hexagonId', + config: { + dataId: csvDataId, + label: 'H3 Hexagon 1', + color: [255, 153, 31], + columns: {hex_id: 'hex_id'}, + isVisible: true + } + }, + { + id: 'h3-layer-2', + type: 'hexagonId', + config: { + dataId: csvDataId, + label: 'H3 Hexagon 2', + color: [255, 153, 31], + columns: {hex_id: 'hex_id'}, + isVisible: true + } + } + ] + } + } + } + } + ] + } + ]); + return prepareState; +} + /** * Mock state will contain 1 heatmap, 1 point and 1 arc layer, 1 hexbin layer and 1 time filter * @param {*} state @@ -457,6 +509,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 +845,8 @@ export const StateWTrips = mockStateWithTripData(); export const StateWTripGeojson = mockStateWithTripGeojson(); export const StateWTooltipFormat = mockStateWithTooltipFormat(); export const StateWH3Layer = mockStateWithH3Layer(); +export const StateWMultiH3Layers = mockStateWithMultipleH3Layers(); +export const StateWithGeocoderDataset = mockStateWithGeocoderDataset(); export const expectedSavedTripLayer = { id: 'trip-0', diff --git a/test/node/reducers/vis-state-test.js b/test/node/reducers/vis-state-test.js index 6a79053477..52d70bb233 100644 --- a/test/node/reducers/vis-state-test.js +++ b/test/node/reducers/vis-state-test.js @@ -4714,6 +4714,8 @@ test('#visStateReducer -> SORT_TABLE_COLUMN', t => { {'gps_data.lat': 'DESCENDING'}, 'should correctly sort' ); + assertDatasetIsTable(t, nextState5.datasets[testCsvDataId]); + t.end(); }); @@ -4752,6 +4754,7 @@ test('#visStateReducer -> PIN_TABLE_COLUMN', t => { const addedKeys = newKeys.filter(k => !Object.keys(previousDataset1).includes(k)); t.deepEqual(addedKeys, ['pinnedColumns'], 'should add pinnedColumns to dataset'); + assertDatasetIsTable(t, nextState1.datasets[testCsvDataId]); t.deepEqual( nextState1.datasets[testCsvDataId].pinnedColumns, @@ -4764,6 +4767,7 @@ test('#visStateReducer -> PIN_TABLE_COLUMN', t => { nextState1, VisStateActions.pinTableColumn(testCsvDataId, 'gps_data.lat') ); + assertDatasetIsTable(t, nextState2.datasets[testCsvDataId]); t.deepEqual( nextState2.datasets[testCsvDataId].pinnedColumns, 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(); +}); diff --git a/webpack/build_types.js b/webpack/build_types.js new file mode 100644 index 0000000000..4a54c0c588 --- /dev/null +++ b/webpack/build_types.js @@ -0,0 +1,58 @@ +// Copyright (c) 2020 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. + +const resolve = require('path').resolve; +const DtsBundleWebpack = require('dts-bundle-webpack'); + +const SRC_DIR = resolve(__dirname, '../src'); +const OUTPUT_DIR = resolve(__dirname, '../dist'); + +const LIBRARY_BUNDLE_CONFIG = env => ({ + // Silence warnings about big bundles + stats: { + warnings: false + }, + + // let's put everything in + module: { + rules: [ + { + test: /\.js$/, + loader: 'babel-loader', + include: [SRC_DIR] + } + ] + }, + + node: { + fs: 'empty' + }, + + plugins: [ + new DtsBundleWebpack({ + name: 'kepler.gl', + main: `${SRC_DIR}/index.d.ts`, + out: `${OUTPUT_DIR}/types.d.ts`, + outputAsModuleFolder: true + }) + ] +}); + +module.exports = env => LIBRARY_BUNDLE_CONFIG(env); diff --git a/yarn.lock b/yarn.lock index 1a48a5c634..87c24592ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2954,11 +2954,24 @@ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-2.1.1.tgz#743fdc821c81f86537cbfece07093ac39b4bc342" integrity sha512-9MVYlmIgmRR31C5b4FVSWtuMmBHh2mOWQYfl7XAYOa8dsnb7iEmUmRSWSFgXFtkjxO65d7hTUHQC+RhR/9IWFg== +"@types/detect-indent@0.1.30": + version "0.1.30" + resolved "https://registry.yarnpkg.com/@types/detect-indent/-/detect-indent-0.1.30.tgz#dc682bb412b4e65ba098e70edad73b4833fb910d" + integrity sha1-3GgrtBK05lugmOcO2tc7SDP7kQ0= + "@types/geojson@^7946.0.7": version "7946.0.7" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== +"@types/glob@5.0.30": + version "5.0.30" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.30.tgz#1026409c5625a8689074602808d082b2867b8a51" + integrity sha1-ECZAnFYlqGiQdGAoCNCCsoZ7ilE= + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -3038,11 +3051,21 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.1.tgz#283f669ff76d7b8260df8ab7a4262cc83d988256" integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg== +"@types/mkdirp@0.3.29": + version "0.3.29" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.3.29.tgz#7f2ad7ec55f914482fc9b1ec4bb1ae6028d46066" + integrity sha1-fyrX7FX5FEgvybHsS7GuYCjUYGY= + "@types/node@*": version "14.14.9" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.9.tgz#04afc9a25c6ff93da14deabd65dc44485b53c8d6" integrity sha512-JsoLXFppG62tWTklIoO4knA+oDTYsmqWxHRvd4lpmfQRNhX6osheUOWETP2jMoV/2bEHuMra8Pp3Dmo/stBFcw== +"@types/node@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.0.tgz#acaa89247afddc7967e9902fd11761dadea1a555" + integrity sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -4894,7 +4917,7 @@ comma-separated-tokens@^1.0.1: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz#632b80b6117867a158f1080ad498b2fbe7e3f5ea" integrity sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw== -commander@2, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0: +commander@2, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@^2.9.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== @@ -5722,6 +5745,14 @@ detect-file@^1.0.0: resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= +detect-indent@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-0.2.0.tgz#042914498979ac2d9f3c73e4ff3e6877d3bc92b6" + integrity sha1-BCkUSYl5rC2fPHPk/z5od9O8krY= + dependencies: + get-stdin "^0.1.0" + minimist "^0.1.0" + detect-libc@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -6051,6 +6082,27 @@ draco3d@^1.3.6: resolved "https://registry.yarnpkg.com/draco3d/-/draco3d-1.3.6.tgz#e4d9e7d846637775328c903721c932b953dce331" integrity sha512-zZoH5JNcdWDrUb2ks2mbzGDUUPvDaDf1ysTJS2St+3/F/8XcKAX4VKgzPjTP7MfHegHQ7Udv8ovS+R3AgXlH7g== +dts-bundle-webpack@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/dts-bundle-webpack/-/dts-bundle-webpack-1.0.2.tgz#c50439a89e71fd9e42800092561c05d5c3ea9ea6" + integrity sha512-/gBQBu5spW8BsGKyYwZeDb+gzDsipisf4Hg0ERPrrS0661cYajVUHARwvts/vfvG5wuv+p295byoNl2da+Re6w== + dependencies: + dts-bundle "^0.7.3" + +dts-bundle@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/dts-bundle/-/dts-bundle-0.7.3.tgz#372b7bb69c820782e6382f400739a69dced3d59a" + integrity sha1-Nyt7tpyCB4LmOC9ABzmmnc7T1Zo= + dependencies: + "@types/detect-indent" "0.1.30" + "@types/glob" "5.0.30" + "@types/mkdirp" "0.3.29" + "@types/node" "8.0.0" + commander "^2.9.0" + detect-indent "^0.2.0" + glob "^6.0.4" + mkdirp "^0.5.0" + duplexer2@^0.1.2, duplexer2@^0.1.4, duplexer2@~0.1.0, duplexer2@~0.1.2: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" @@ -7313,6 +7365,11 @@ get-port@^4.0.0: resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== +get-stdin@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-0.1.0.tgz#5998af24aafc802d15c82c685657eeb8b10d4a91" + integrity sha1-WZivJKr8gC0VyCxoVlfuuLENSpE= + get-stdin@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398" @@ -7444,7 +7501,7 @@ glob-stream@^6.1.0: to-absolute-glob "^2.0.0" unique-stream "^2.0.2" -glob@^6.0.1: +glob@^6.0.1, glob@^6.0.4: version "6.0.4" resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" integrity sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI= @@ -10194,7 +10251,7 @@ minimist-options@4.1.0: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@1.2.3, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@~1.2.0, minimist@~1.2.5: +minimist@1.2.3, minimist@^0.1.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@~1.2.0, minimist@~1.2.5: version "1.2.3" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.3.tgz#3db5c0765545ab8637be71f333a104a965a9ca3f" integrity sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw==