├── .nvmrc ├── jest.setup.js ├── .eslintrc.json ├── .npmrc ├── packages ├── react-auth │ ├── src │ │ ├── oauth │ │ │ ├── OAuthCallback.d.ts │ │ │ ├── useOAuthLogin.d.ts │ │ │ └── OAuthCallback.js │ │ ├── types.d.ts │ │ ├── index.js │ │ └── index.d.ts │ ├── babel.config.js │ ├── jest.config.js │ ├── webpack.config.js │ └── README.md ├── react-redux │ ├── src │ │ ├── index.js │ │ ├── index.d.ts │ │ └── slices │ │ │ └── oauthSlice.d.ts │ ├── babel.config.js │ ├── jest.config.js │ ├── webpack.config.js │ ├── README.md │ └── __tests__ │ │ └── mockReducerManager.js ├── react-api │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ │ ├── types.js │ │ ├── hooks │ │ │ ├── maskExtensionUtil.js │ │ │ ├── utils.js │ │ │ ├── useCartoLayerProps.d.ts │ │ │ └── useFeaturesCommons.js │ │ ├── index.d.ts │ │ ├── api │ │ │ ├── SQL.d.ts │ │ │ ├── tilejson.js │ │ │ └── lds.js │ │ └── index.js │ ├── README.md │ ├── __tests__ │ │ ├── mockReduxHooks.js │ │ ├── mockSqlApiRequest.js │ │ ├── hooks │ │ │ └── mask-extension-util.test.js │ │ └── api │ │ │ ├── lds.test.js │ │ │ └── tilejson.test.js │ └── webpack.config.js ├── react-core │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ │ ├── utils │ │ │ ├── clientParameter.d.ts │ │ │ ├── assert.js │ │ │ ├── columns.d.ts │ │ │ ├── clientParameter.js │ │ │ ├── throttle.js │ │ │ ├── featureFlags.d.ts │ │ │ ├── debounce.js │ │ │ ├── randomString.js │ │ │ ├── makeIntervalComplete.js │ │ │ ├── geo.d.ts │ │ │ ├── InvalidColumnError.js │ │ │ ├── featureSelectionConstants.js │ │ │ ├── columns.js │ │ │ ├── dateUtils.js │ │ │ ├── featureFlags.js │ │ │ └── requestsUtils.js │ │ ├── operations │ │ │ ├── constants │ │ │ │ ├── FiltersLogicalOperators.d.ts │ │ │ │ ├── SpatialIndexTypes.d.ts │ │ │ │ ├── SpatialIndexTypes.js │ │ │ │ ├── AggregationTypes.d.ts │ │ │ │ ├── GroupDateTypes.d.ts │ │ │ │ ├── Provider.ts │ │ │ │ ├── FiltersLogicalOperators.js │ │ │ │ ├── Provider.js │ │ │ │ ├── AggregationTypes.js │ │ │ │ └── GroupDateTypes.js │ │ │ ├── aggregation.d.ts │ │ │ ├── histogram.d.ts │ │ │ ├── groupBy.d.ts │ │ │ ├── scatterPlot.d.ts │ │ │ ├── groupByDate.d.ts │ │ │ ├── scatterPlot.js │ │ │ ├── groupBy.js │ │ │ └── histogram.js │ │ ├── types.js │ │ ├── filters │ │ │ ├── tileFeatures.d.ts │ │ │ ├── geojsonFeatures.d.ts │ │ │ ├── tileFeaturesSpatialIndex.d.ts │ │ │ ├── FilterTypes.d.ts │ │ │ ├── tileFeaturesGeometries.d.ts │ │ │ ├── geojsonFeatures.js │ │ │ └── tileFeatures.js │ │ └── types.d.ts │ ├── __tests__ │ │ ├── utils │ │ │ ├── throttle.test.js │ │ │ ├── randomString.test.js │ │ │ ├── debounce.test.js │ │ │ ├── makeIntervalComplete.test.js │ │ │ └── transformToTileCoords.test.js │ │ └── operations │ │ │ └── scatterPlot.test.js │ ├── README.md │ └── webpack.config.js ├── react-ui │ ├── babel.config.js │ ├── jest.config.js │ ├── storybook │ │ ├── assets │ │ │ ├── avatar.jpeg │ │ │ ├── figma-custom-icon.png │ │ │ ├── dot.svg │ │ │ ├── carto-logo.svg │ │ │ ├── carto-logo-dark.svg │ │ │ ├── figma.svg │ │ │ ├── carto-symbol.svg │ │ │ ├── mui.svg │ │ │ ├── github.svg │ │ │ └── doc.svg │ │ ├── .storybook │ │ │ ├── preview-head.html │ │ │ ├── main.js │ │ │ └── manager.js │ │ ├── stories │ │ │ ├── organisms │ │ │ │ └── ListItemGuide.stories.mdx │ │ │ ├── widgetsUI │ │ │ │ ├── legend │ │ │ │ │ ├── LegendProportion.stories.js │ │ │ │ │ ├── LegendCategories.stories.js │ │ │ │ │ └── LegendRamp.stories.js │ │ │ │ ├── LoadingTemplateWithSwitch.js │ │ │ │ ├── NoDataAlert.stories.js │ │ │ │ ├── RangeWidgetUI.stories.js │ │ │ │ └── ComparativeFormulaWidgetUI.stories.js │ │ │ ├── foundations │ │ │ │ ├── BreakpointsGuide.stories.mdx │ │ │ │ ├── Breakpoints.stories.js │ │ │ │ ├── PaletteGuide.stories.mdx │ │ │ │ ├── SpacingGuide.stories.mdx │ │ │ │ └── TypographyGuide.stories.mdx │ │ │ ├── atoms │ │ │ │ ├── LabelWithIndicatorGuide.stories.mdx │ │ │ │ └── HelperText.stories.js │ │ │ ├── widgets │ │ │ │ └── utils.js │ │ │ └── icons │ │ │ │ └── CartoIcons.stories.js │ │ └── utils │ │ │ ├── docStyles.css │ │ │ └── utils.js │ ├── src │ │ ├── widgets │ │ │ ├── TimeSeriesWidgetUI │ │ │ │ └── utils │ │ │ │ │ └── constants.js │ │ │ ├── legend │ │ │ │ ├── legend-types │ │ │ │ │ ├── LegendTypes.js │ │ │ │ │ └── LegendIcon.js │ │ │ │ └── LegendLayerTitle.js │ │ │ ├── utils │ │ │ │ ├── formatterUtils.js │ │ │ │ ├── chartConstants.js │ │ │ │ └── detectTouchScreen.js │ │ │ ├── FormulaWidgetUI │ │ │ │ └── FormulaSkeleton.js │ │ │ ├── TableWidgetUI │ │ │ │ └── Skeleton │ │ │ │ │ └── TableSkeletonRow.js │ │ │ ├── useSkeleton.js │ │ │ ├── comparative │ │ │ │ └── ComparativeFormulaWidgetUI │ │ │ │ │ └── FormulaLabel.js │ │ │ ├── BarWidgetUI │ │ │ │ └── BarSkeleton.js │ │ │ ├── CategoryWidgetUI │ │ │ │ └── CategorySkeleton.js │ │ │ ├── RangeWidgetUI │ │ │ │ └── RangeSkeleton.js │ │ │ ├── NoDataAlert.js │ │ │ ├── HistogramWidgetUI │ │ │ │ └── HistogramSkeleton.js │ │ │ └── WrapperWidgetUI.d.ts │ │ ├── localization │ │ │ ├── index.js │ │ │ └── localeUtils.js │ │ ├── components │ │ │ ├── atoms │ │ │ │ ├── PasswordField.d.ts │ │ │ │ ├── Button.d.ts │ │ │ │ ├── LabelWithIndicator.d.ts │ │ │ │ ├── ToggleButtonGroup.d.ts │ │ │ │ ├── Button.js │ │ │ │ ├── SelectField.d.ts │ │ │ │ ├── Typography.d.ts │ │ │ │ ├── Typography.js │ │ │ │ └── PasswordField.js │ │ │ ├── molecules │ │ │ │ ├── Avatar.d.ts │ │ │ │ ├── Menu.d.ts │ │ │ │ ├── MenuList.d.ts │ │ │ │ ├── MenuItem.d.ts │ │ │ │ ├── AccordionGroup.d.ts │ │ │ │ ├── Alert.d.ts │ │ │ │ ├── UploadField │ │ │ │ │ ├── UploadField.d.ts │ │ │ │ │ ├── UploadFieldBase.d.ts │ │ │ │ │ ├── StyledUploadField.js │ │ │ │ │ └── FilesAction.js │ │ │ │ ├── MultipleSelectField │ │ │ │ │ ├── MultipleSelectField.d.ts │ │ │ │ │ └── Filters.js │ │ │ │ ├── Menu.js │ │ │ │ ├── Autocomplete.d.ts │ │ │ │ ├── Avatar.js │ │ │ │ └── MenuList.js │ │ │ └── organisms │ │ │ │ └── AppBar │ │ │ │ ├── BrandLogo.js │ │ │ │ ├── BrandText.js │ │ │ │ ├── SecondaryText.js │ │ │ │ ├── AppBar.d.ts │ │ │ │ └── BurgerMenu.js │ │ ├── theme │ │ │ ├── themeUtils.js │ │ │ └── themeConstants.js │ │ ├── assets │ │ │ ├── icons │ │ │ │ ├── CircleIcon.js │ │ │ │ ├── ArrowDropIcon.js │ │ │ │ ├── CursorIcon.js │ │ │ │ ├── UploadIcon.js │ │ │ │ ├── SearchIcon.js │ │ │ │ ├── RectangleIcon.js │ │ │ │ ├── LayerIcon.js │ │ │ │ ├── PolygonIcon.js │ │ │ │ ├── LassoIcon.js │ │ │ │ └── OpacityIcon.js │ │ │ ├── index.js │ │ │ └── images │ │ │ │ └── GraphLine.js │ │ ├── utils │ │ │ └── palette.d.ts │ │ ├── custom-components │ │ │ └── AnimatedNumber.js │ │ └── hooks │ │ │ ├── useAnimatedNumber.js │ │ │ └── useImperativeIntl.js │ ├── firebase.json │ ├── README.md │ ├── __tests__ │ │ └── widgets │ │ │ ├── utils │ │ │ ├── formatterUtils.test.js │ │ │ └── testUtils.js │ │ │ ├── PieWidgetUI.test.js │ │ │ ├── ScatterPlotWidget.test.js │ │ │ ├── testUtils.js │ │ │ ├── HistogramWidgetUI.test.js │ │ │ └── ComparativePieWidgetUI.test.js │ └── webpack.config.js ├── react-basemaps │ ├── babel.config.js │ ├── src │ │ ├── index.js │ │ ├── index.d.ts │ │ ├── types.d.ts │ │ └── basemaps │ │ │ └── basemaps.d.ts │ ├── jest.config.js │ ├── webpack.config.js │ ├── README.md │ └── __tests__ │ │ └── basemaps.test.js ├── react-widgets │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ │ ├── widgets │ │ │ ├── utils │ │ │ │ ├── constants.js │ │ │ │ ├── sortLayers.js │ │ │ │ ├── validateCoordinates.js │ │ │ │ ├── WidgetWithAlert.js │ │ │ │ └── propTypesFns.js │ │ │ └── TimeSeriesWidget.d.ts │ │ ├── hooks │ │ │ ├── useCustomCompareMemo.js │ │ │ ├── useCustomCompareEffect.js │ │ │ ├── useWidgetFilterValues.js │ │ │ ├── useWidgetSource.js │ │ │ ├── useSourceFilters.js │ │ │ └── useStats.js │ │ ├── models │ │ │ ├── index.js │ │ │ ├── utils.d.ts │ │ │ ├── RangeModel.js │ │ │ ├── CategoryModel.js │ │ │ ├── HistogramModel.js │ │ │ ├── FormulaModel.js │ │ │ └── ScatterPlotModel.js │ │ ├── layers │ │ │ └── MaskLayer.js │ │ └── index.js │ ├── README.md │ ├── __tests__ │ │ ├── mockSqlApiRequest.js │ │ ├── mockReduxHooks.js │ │ └── layers │ │ │ └── FeatureSelectionLayer.test.js │ └── webpack.config.js └── react-workers │ ├── babel.config.js │ ├── jest.config.js │ ├── src │ ├── index.d.ts │ ├── index.js │ ├── workerPool.d.ts │ ├── workerMethods.d.ts │ ├── workerMethods.js │ ├── workers │ │ └── features.worker.js │ └── workerPool.js │ ├── README.md │ └── webpack.config.js ├── lerna.json ├── .prettierrc ├── nx.json ├── firebase.json ├── babel.config.base.js ├── .editorconfig ├── .gitignore ├── copy-packages.sh ├── docs └── upgrade-guide.md ├── webpack.base.js ├── tsconfig.json ├── .github └── pull_request_template.md ├── LICENSE.MD ├── patches └── h3-js+3.7.2.patch └── scripts └── mergeCoverage.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app" 3 | } 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @cartodb:registry=https://npm.pkg.github.com 2 | -------------------------------------------------------------------------------- /packages/react-auth/src/oauth/OAuthCallback.d.ts: -------------------------------------------------------------------------------- 1 | export default function OAuthCallback(): null; -------------------------------------------------------------------------------- /packages/react-redux/src/index.js: -------------------------------------------------------------------------------- 1 | export * from './slices/cartoSlice'; 2 | export * from './slices/oauthSlice'; 3 | -------------------------------------------------------------------------------- /packages/react-api/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-auth/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-core/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-redux/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-ui/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-ui/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-api/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-auth/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-basemaps/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-basemaps/src/index.js: -------------------------------------------------------------------------------- 1 | export * from './basemaps/basemaps'; 2 | 3 | export { GoogleMap } from './basemaps/GoogleMap'; 4 | -------------------------------------------------------------------------------- /packages/react-core/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/clientParameter.d.ts: -------------------------------------------------------------------------------- 1 | export function getClient(): string 2 | export function setClient(c: string): void 3 | -------------------------------------------------------------------------------- /packages/react-redux/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-widgets/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-widgets/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-workers/babel.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../babel.config.base'); 2 | module.exports = { 3 | ...base 4 | }; 5 | -------------------------------------------------------------------------------- /packages/react-workers/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-basemaps/jest.config.js: -------------------------------------------------------------------------------- 1 | const base = require('../../jest.config.base'); 2 | 3 | module.exports = { 4 | ...base 5 | }; 6 | -------------------------------------------------------------------------------- /packages/react-workers/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { Methods } from './workerMethods'; 2 | export { executeTask, removeWorker } from './workerPool'; -------------------------------------------------------------------------------- /packages/react-redux/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './slices/cartoSlice'; 2 | export * from './slices/oauthSlice'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/avatar.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/carto-react/HEAD/packages/react-ui/storybook/assets/avatar.jpeg -------------------------------------------------------------------------------- /packages/react-workers/src/index.js: -------------------------------------------------------------------------------- 1 | export { Methods } from './workerMethods'; 2 | export { executeTask, removeWorker } from './workerPool'; 3 | -------------------------------------------------------------------------------- /packages/react-auth/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export type OauthApp = { 2 | clientId: string, 3 | scopes: string[], 4 | authorizeEndPoint: string 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/FiltersLogicalOperators.d.ts: -------------------------------------------------------------------------------- 1 | export enum FiltersLogicalOperators { 2 | AND = 'and', 3 | OR = 'or' 4 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "useWorkspaces": true, 7 | "version": "3.1.0-alpha.16" 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/assert.js: -------------------------------------------------------------------------------- 1 | export function assert(condition, message) { 2 | if (!condition) { 3 | throw new Error(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/TimeSeriesWidgetUI/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const CHART_TYPES = Object.freeze({ 2 | LINE: 'line', 3 | BAR: 'bar' 4 | }); 5 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/SpatialIndexTypes.d.ts: -------------------------------------------------------------------------------- 1 | export enum SpatialIndex { 2 | H3 = 'h3', 3 | S2 = 's2', 4 | QUADBIN = 'quadbin' 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/figma-custom-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CartoDB/carto-react/HEAD/packages/react-ui/storybook/assets/figma-custom-icon.png -------------------------------------------------------------------------------- /packages/react-auth/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as OAuthCallback } from './oauth/OAuthCallback'; 2 | export { default as useOAuthLogin } from './oauth/useOAuthLogin'; 3 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/SpatialIndexTypes.js: -------------------------------------------------------------------------------- 1 | export const SpatialIndex = Object.freeze({ 2 | H3: 'h3', 3 | S2: 's2', 4 | QUADBIN: 'quadbin' 5 | }); 6 | -------------------------------------------------------------------------------- /packages/react-core/src/types.js: -------------------------------------------------------------------------------- 1 | export const TILE_FORMATS = Object.freeze({ 2 | MVT: 'mvt', 3 | JSON: 'json', 4 | GEOJSON: 'geojson', 5 | BINARY: 'binary' 6 | }); 7 | -------------------------------------------------------------------------------- /packages/react-basemaps/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './basemaps/basemaps'; 2 | 3 | export { GoogleMap } from './basemaps/GoogleMap'; 4 | 5 | export { GMap } from './types'; 6 | 7 | -------------------------------------------------------------------------------- /packages/react-widgets/src/widgets/utils/constants.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_INVALID_COLUMN_ERR = 2 | 'One or more columns used in this widget are not available in the data source.'; 3 | -------------------------------------------------------------------------------- /packages/react-core/src/filters/tileFeatures.d.ts: -------------------------------------------------------------------------------- 1 | import { TileFeatures, TileFeaturesResponse } from '../types'; 2 | 3 | export function tileFeatures(arg: TileFeatures): TileFeaturesResponse; -------------------------------------------------------------------------------- /packages/react-auth/src/oauth/useOAuthLogin.d.ts: -------------------------------------------------------------------------------- 1 | import { OauthApp } from '../types'; 2 | 3 | export default function useOAuthLogin(oauthApp: OauthApp, onParamsRefreshed: Function): Function[]; 4 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/columns.d.ts: -------------------------------------------------------------------------------- 1 | export function getColumnNameFromGeoColumn(geoColumn: string): string | null; 2 | export function getSpatialIndexFromGeoColumn(geoColumn: string): string | null; -------------------------------------------------------------------------------- /packages/react-ui/src/localization/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import es from './es'; 3 | import id from './id'; 4 | 5 | export const messages = { 6 | en, 7 | es, 8 | id 9 | }; 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 90, 3 | "arrowParens": "always", 4 | "semi": true, 5 | "tabWidth": 2, 6 | "singleQuote": true, 7 | "jsxSingleQuote": true, 8 | "trailingComma": "none" 9 | } 10 | 11 | -------------------------------------------------------------------------------- /packages/react-auth/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as OAuthCallback } from './oauth/OAuthCallback'; 2 | export { default as useOAuthLogin } from './oauth/useOAuthLogin'; 3 | 4 | export { OauthApp } from './types'; 5 | -------------------------------------------------------------------------------- /packages/react-auth/src/oauth/OAuthCallback.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Component to attend OAuth callbacks on /oauthCallback 3 | * @exports OAuthCallback 4 | */ 5 | export default function OAuthCallback() { 6 | return null; 7 | } 8 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/AggregationTypes.d.ts: -------------------------------------------------------------------------------- 1 | export enum AggregationTypes { 2 | COUNT = 'count', 3 | AVG = 'avg', 4 | MIN = 'min', 5 | MAX = 'max', 6 | SUM = 'sum', 7 | CUSTOM = 'custom' 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/clientParameter.js: -------------------------------------------------------------------------------- 1 | // Default client 2 | let client = 'c4react'; 3 | 4 | export function getClient() { 5 | return client; 6 | } 7 | 8 | export function setClient(c) { 9 | client = c; 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-basemaps/src/types.d.ts: -------------------------------------------------------------------------------- 1 | export type GMap = { 2 | basemap: object, 3 | viewState: object, 4 | layers: object[], 5 | getTooltip: Function, 6 | onResize: Function, 7 | onViewStateChange: Function, 8 | apiKey: string 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-api/src/types.js: -------------------------------------------------------------------------------- 1 | export const MAP_TYPES = Object.freeze({ 2 | TABLE: 'table', 3 | QUERY: 'query', 4 | TILESET: 'tileset' 5 | }); 6 | 7 | export const API_VERSIONS = Object.freeze({ 8 | V1: 'v1', 9 | V2: 'v2', 10 | V3: 'v3' 11 | }); 12 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/legend/legend-types/LegendTypes.js: -------------------------------------------------------------------------------- 1 | const LEGEND_TYPES = { 2 | CATEGORY: 'category', 3 | ICON: 'icon', 4 | CONTINUOUS_RAMP: 'continuous_ramp', 5 | BINS: 'bins', 6 | PROPORTION: 'proportion' 7 | }; 8 | export default LEGEND_TYPES; 9 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksRunnerOptions": { 3 | "default": { 4 | "runner": "nx/tasks-runners/default", 5 | "options": { 6 | "cacheableOperations": [ 7 | "build", 8 | "test" 9 | ] 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/GroupDateTypes.d.ts: -------------------------------------------------------------------------------- 1 | export enum GroupDateTypes { 2 | YEARS = 'year', 3 | MONTHS = 'month', 4 | WEEKS = 'week', 5 | DAYS = 'day', 6 | HOURS = 'hour', 7 | MINUTES = 'minute', 8 | SECONDS = 'second' 9 | } 10 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | export function throttle(fn, ms) { 2 | var lastTime = 0; 3 | return function () { 4 | var now = new Date(); 5 | if (now - lastTime >= ms) { 6 | fn(); 7 | lastTime = now; 8 | } 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/utils/formatterUtils.js: -------------------------------------------------------------------------------- 1 | export function processFormatterRes(formatterRes) { 2 | return typeof formatterRes === 'object' 3 | ? `${formatterRes.prefix || ''}${formatterRes.value}${formatterRes.suffix || ''}` 4 | : formatterRes; 5 | } 6 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/Provider.ts: -------------------------------------------------------------------------------- 1 | export enum Provider { 2 | BigQuery = 'bigquery', 3 | Redshift = 'redshift', 4 | Postgres = 'postgres', 5 | Snowflake = 'snowflake', 6 | Databricks = 'databricks', 7 | DatabricksRest = 'databricksRest' 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/featureFlags.d.ts: -------------------------------------------------------------------------------- 1 | export enum Flags { 2 | REMOTE_WIDGETS = '2023-remote-widgets' 3 | } 4 | export function setFlags(flags: Record | string[]): void 5 | export function clearFlags(): void 6 | export function hasFlag(flag: string): boolean 7 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/PasswordField.d.ts: -------------------------------------------------------------------------------- 1 | import { TextFieldProps } from '@mui/material/TextField'; 2 | 3 | export type PasswordFieldProps = TextFieldProps; 4 | 5 | declare const PasswordField: (props: PasswordFieldProps) => JSX.Element; 6 | export default PasswordField; 7 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/utils/chartConstants.js: -------------------------------------------------------------------------------- 1 | export const OTHERS_CATEGORY_NAME = 'Others'; 2 | 3 | // 'RANKING' orders the data by value and 'FIXED' keeps the order present in the original data 4 | export const ORDER_TYPES = { 5 | RANKING: 'ranking', 6 | FIXED: 'fixed' 7 | }; 8 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/FiltersLogicalOperators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for the different logical operator applied to source filters 3 | * @enum {string} 4 | * @readonly 5 | */ 6 | export const FiltersLogicalOperators = Object.freeze({ 7 | AND: 'and', 8 | OR: 'or' 9 | }); 10 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export function debounce(fn, ms) { 2 | let timer; 3 | return (...args) => { 4 | clearTimeout(timer); 5 | timer = setTimeout(() => { 6 | timer = null; 7 | fn.apply(this, args); 8 | }, ms); 9 | return timer; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-workers/src/workerPool.d.ts: -------------------------------------------------------------------------------- 1 | import { Methods } from './workerMethods'; 2 | import { TileFeatures } from '@carto/react-core'; 3 | 4 | export function executeTask(source: string, method: Methods, params?: TileFeatures): Promise; 5 | 6 | export function removeWorker(source: string): void; 7 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/Button.d.ts: -------------------------------------------------------------------------------- 1 | import { ButtonProps, ButtonTypeMap } from '@mui/material/Button'; 2 | import { OverridableComponent } from '@mui/material/OverridableComponent'; 3 | 4 | export { ButtonProps }; 5 | 6 | declare const Button: OverridableComponent; 7 | export default Button; 8 | -------------------------------------------------------------------------------- /packages/react-widgets/src/hooks/useCustomCompareMemo.js: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | export default function useCustomCompareMemo(value, isEqual) { 4 | const ref = useRef(value); 5 | 6 | if (!isEqual(ref.current, value)) { 7 | ref.current = value; 8 | } 9 | 10 | return ref.current; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/Avatar.d.ts: -------------------------------------------------------------------------------- 1 | import { AvatarProps as MuiAvatarProps } from '@mui/material/Avatar'; 2 | 3 | export type AvatarProps = MuiAvatarProps & { 4 | size?: 'large' | 'medium' | 'small' | 'xsmall'; 5 | }; 6 | 7 | declare const Avatar: (props: AvatarProps) => JSX.Element; 8 | export default Avatar; 9 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "storybook-static", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/Menu.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuProps as MuiMenuProps } from '@mui/material'; 2 | 3 | export type MenuProps = MuiMenuProps & { 4 | extended?: boolean; 5 | width?: string; 6 | height?: string; 7 | }; 8 | 9 | declare const Menu: (props: MenuProps) => JSX.Element; 10 | export default Menu; 11 | -------------------------------------------------------------------------------- /packages/react-ui/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "target": "default", 4 | "public": "storybook-static", 5 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 6 | "rewrites": [ 7 | { 8 | "source": "**", 9 | "destination": "/index.html" 10 | } 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/randomString.js: -------------------------------------------------------------------------------- 1 | export function randomString(length) { 2 | let text = ''; 3 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 | for (let i = 0; i < length; i++) { 5 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 6 | } 7 | return text; 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/FormulaWidgetUI/FormulaSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Skeleton } from '@mui/material'; 3 | 4 | const FormulaSkeleton = () => { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | }; 11 | 12 | export default FormulaSkeleton; 13 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /babel.config.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { 4 | targets: { node: 'current' }, 5 | forceAllTransforms: true, // es5 6 | }], 7 | '@babel/preset-react' 8 | ], 9 | plugins: [ 10 | "@babel/plugin-proposal-nullish-coalescing-operator", 11 | '@babel/plugin-transform-runtime' 12 | ] 13 | }; 14 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/MenuList.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuListProps as MuiMenuListProps } from '@mui/material'; 2 | 3 | export type MenuListProps = MuiMenuListProps & { 4 | extended?: boolean; 5 | width?: string; 6 | height?: string; 7 | }; 8 | 9 | declare const MenuList: (props: MenuListProps) => JSX.Element; 10 | export default MenuList; 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/LabelWithIndicator.d.ts: -------------------------------------------------------------------------------- 1 | export type LabelWithIndicatorProps = { 2 | label: React.ReactNode; 3 | type?: 'optional' | 'required'; 4 | icon?: React.ReactNode; 5 | inheritSize?: boolean; 6 | }; 7 | 8 | declare const LabelWithIndicator: (props: LabelWithIndicatorProps) => JSX.Element; 9 | export default LabelWithIndicator; 10 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/aggregation.d.ts: -------------------------------------------------------------------------------- 1 | import { AggregationFunctions } from '../types'; 2 | import { AggregationTypes } from './constants/AggregationTypes'; 3 | 4 | export const aggregationFunctions: AggregationFunctions; 5 | 6 | export function aggregate( 7 | feature: Record, 8 | keys?: string[], 9 | joinOperation?: AggregationTypes 10 | ); 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | 2 | # https://editorconfig.org 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | max_line_length = 80 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | max_line_length = 0 16 | trim_trailing_whitespace = false 17 | 18 | [COMMIT_EDITMSG] 19 | max_line_length = 0 20 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: ['../**/*.stories.mdx', '../**/*.stories.@(js|jsx|ts|tsx)'], 3 | addons: [ 4 | '@storybook/addon-links', 5 | '@storybook/addon-essentials', 6 | 'storybook-addon-designs', 7 | '@storybook/addon-viewport', 8 | '@etchteam/storybook-addon-status' 9 | ], 10 | staticDirs: ['../assets'] 11 | }; 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | /reports 4 | /.nyc_output 5 | /.vscode 6 | 7 | /packages/*/node_modules 8 | /packages/*/coverage 9 | /packages/*/dist 10 | /packages/*/yarn-error.log 11 | 12 | /packages/react-ui/storybook-static 13 | 14 | .DS_Store 15 | .idea 16 | 17 | # Firebase 18 | firebase-debug.log* 19 | firebase-debug.*.log* 20 | 21 | # Firebase cache 22 | .firebase/ 23 | 24 | 25 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/Provider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for the different connections providers 3 | * @enum {string} 4 | * @readonly 5 | */ 6 | export const Provider = Object.freeze({ 7 | BigQuery: 'bigquery', 8 | Redshift: 'redshift', 9 | Postgres: 'postgres', 10 | Snowflake: 'snowflake', 11 | Databricks: 'databricks', 12 | DatabricksRest: 'databricksRest' 13 | }); 14 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/histogram.d.ts: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from './constants/AggregationTypes'; 2 | import { HistogramFeature } from '../types'; 3 | 4 | export function histogram(args: { 5 | data: Record[]; 6 | valuesColumns?: string[]; 7 | joinOperation?: AggregationTypes; 8 | ticks: number[]; 9 | operation?: AggregationTypes; 10 | }): HistogramFeature; 11 | -------------------------------------------------------------------------------- /packages/react-api/src/hooks/maskExtensionUtil.js: -------------------------------------------------------------------------------- 1 | import { MASK_ID } from '@carto/react-core/'; 2 | import { MaskExtension } from '@deck.gl/extensions'; 3 | 4 | const maskExtension = new MaskExtension(); 5 | 6 | export function getMaskExtensionProps(maskPolygon) { 7 | return { 8 | maskId: Boolean(maskPolygon && maskPolygon.length) && MASK_ID, 9 | extensions: [maskExtension] 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/react-ui/src/theme/themeUtils.js: -------------------------------------------------------------------------------- 1 | import { createSpacing } from '@mui/system'; 2 | import { SPACING } from './themeConstants'; 3 | 4 | // Create spacing for theming 5 | export const getSpacing = createSpacing(SPACING); 6 | 7 | // Convert pixels to rem 8 | export function getPixelToRem(px) { 9 | const fontBase = 16; 10 | const rem = (1 / fontBase) * px + 'rem'; 11 | 12 | return rem; 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/groupBy.d.ts: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from './constants/AggregationTypes'; 2 | import { GroupByFeature } from '../types'; 3 | 4 | export function groupValuesByColumn(args: { 5 | data: Record[]; 6 | valuesColumns?: string[]; 7 | joinOperation?: AggregationTypes; 8 | keysColumn?: string; 9 | operation?: AggregationTypes; 10 | }): GroupByFeature; 11 | -------------------------------------------------------------------------------- /packages/react-core/__tests__/utils/throttle.test.js: -------------------------------------------------------------------------------- 1 | import { throttle } from '../../src/utils/throttle'; 2 | 3 | describe('throttle', () => { 4 | test('should be executed once', () => { 5 | const mockedFunction = jest.fn(); 6 | const throttledFn = throttle(mockedFunction, 100); 7 | throttledFn(); 8 | throttledFn(); 9 | 10 | expect(mockedFunction).toHaveBeenCalledTimes(1); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/makeIntervalComplete.js: -------------------------------------------------------------------------------- 1 | export function makeIntervalComplete(values) { 2 | return values.map((val) => { 3 | if (val[0] === undefined || val[0] === null) { 4 | return [Number.MIN_SAFE_INTEGER, val[1]]; 5 | } 6 | 7 | if (val[1] === undefined || val[1] === null) { 8 | return [val[0], Number.MAX_SAFE_INTEGER]; 9 | } 10 | 11 | return val; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/CircleIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function CircleIcon(props) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/utils/palette.d.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from '@mui/material'; 2 | 3 | export function getCartoColorStylePropsForItem( 4 | theme: Theme, 5 | index: number 6 | ): { backgroundColor: string; color: string }; 7 | 8 | export function getColorByCategory( 9 | category: string | null | undefined, 10 | props: { palette: string; fallbackColor: string; colorMapping: { [x: string]: string } } 11 | ): string; 12 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/.storybook/manager.js: -------------------------------------------------------------------------------- 1 | // ./storybook/manager.ts 2 | import { addons } from '@storybook/addons'; 3 | import { create } from '@storybook/theming'; 4 | import { version } from '../../package.json'; 5 | 6 | const theme = create({ 7 | base: 'light', 8 | 9 | // Brand 10 | brandTitle: `CARTO DS ${version}`, 11 | brandImage: undefined 12 | }); 13 | 14 | addons.setConfig({ 15 | theme 16 | }); 17 | -------------------------------------------------------------------------------- /packages/react-widgets/src/hooks/useCustomCompareEffect.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export default function useCustomCompareEffect(effect, deps, depsEqual) { 4 | const ref = useRef(); 5 | 6 | if (!ref.current || !depsEqual(deps, ref.current)) { 7 | ref.current = deps; 8 | } 9 | 10 | // eslint-disable-next-line react-hooks/exhaustive-deps 11 | useEffect(effect, ref.current); 12 | } 13 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/scatterPlot.d.ts: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from './constants/AggregationTypes'; 2 | import { ScatterPlotFeature } from '../types'; 3 | 4 | export function scatterPlot(args: { 5 | data: Record[]; 6 | xAxisColumns: string[]; 7 | xAxisJoinOperation?: AggregationTypes; 8 | yAxisColumns: string[]; 9 | yAxisJoinOperation?: AggregationTypes; 10 | }): ScatterPlotFeature; 11 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/index.js: -------------------------------------------------------------------------------- 1 | export { getFormula } from './FormulaModel'; 2 | export { getHistogram } from './HistogramModel'; 3 | export { getCategories } from './CategoryModel'; 4 | export { geocodeStreetPoint } from './GeocodingModel'; 5 | export { getScatter, HARD_LIMIT as SCATTER_PLOT_HARD_LIMIT } from './ScatterPlotModel'; 6 | export { getTimeSeries } from './TimeSeriesModel'; 7 | export { getTable } from './TableModel'; 8 | -------------------------------------------------------------------------------- /packages/react-api/src/hooks/utils.js: -------------------------------------------------------------------------------- 1 | export function throwError(error) { 2 | if (error.name === 'DataCloneError') 3 | throw new Error( 4 | `DataCloneError: Unable to retrieve GeoJSON features. Please check that your query is returning a column called "geom" or that you are using the geoColumn property to indicate the geometry column in the table.` 5 | ); 6 | if (error.name === 'AbortError') return; 7 | throw error; 8 | } 9 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/MenuItem.d.ts: -------------------------------------------------------------------------------- 1 | import { MenuItemProps as MuiMenuItemProps } from '@mui/material'; 2 | 3 | export type MenuItemProps = MuiMenuItemProps & { 4 | subtitle?: boolean; 5 | destructive?: boolean; 6 | extended?: boolean; 7 | iconColor?: 'primary' | 'default'; 8 | fixed?: boolean; 9 | }; 10 | 11 | declare const MenuItem: (props: MenuItemProps) => JSX.Element; 12 | export default MenuItem; 13 | -------------------------------------------------------------------------------- /packages/react-api/src/hooks/useCartoLayerProps.d.ts: -------------------------------------------------------------------------------- 1 | import { SourceProps, LayerConfig, UseCartoLayerFilterProps } from '../types'; 2 | 3 | interface UseCartoLayerProps { 4 | source: SourceProps & { id: string }; 5 | layerConfig?: LayerConfig; 6 | uniqueIdProperty?: string; 7 | viewportFeatures?: boolean; 8 | } 9 | 10 | export default function useCartoLayerProps( 11 | props: UseCartoLayerProps 12 | ): UseCartoLayerFilterProps; 13 | -------------------------------------------------------------------------------- /packages/react-core/src/filters/geojsonFeatures.d.ts: -------------------------------------------------------------------------------- 1 | import { FeatureCollection, Polygon, MultiPolygon } from 'geojson'; 2 | import { Viewport, TileFeaturesResponse } from '../types'; 3 | 4 | type GeojsonFeaturesArgs = { 5 | geojson: FeatureCollection, 6 | viewport?: Viewport, 7 | geometry?: Polygon | MultiPolygon, 8 | uniqueIdProperty?: string 9 | } 10 | 11 | export function geojsonFeatures(arg: GeojsonFeaturesArgs): TileFeaturesResponse; -------------------------------------------------------------------------------- /packages/react-core/src/utils/geo.d.ts: -------------------------------------------------------------------------------- 1 | import { Viewport } from '../types'; 2 | import { Polygon, MultiPolygon } from 'geojson'; 3 | 4 | export function getGeometryToIntersect(viewport: Viewport | null, geometry: Polygon | MultiPolygon | null): Polygon | MultiPolygon | null; 5 | 6 | export function isGlobalViewport(viewport: Viewport | null): boolean; 7 | 8 | export function normalizeGeometry(geometry: Polygon | MultiPolygon | null): Polygon | MultiPolygon | null -------------------------------------------------------------------------------- /packages/react-core/src/filters/tileFeaturesSpatialIndex.d.ts: -------------------------------------------------------------------------------- 1 | import { Polygon, MultiPolygon } from 'geojson'; 2 | import { SpatialIndex } from "../operations/constants/SpatialIndexTypes"; 3 | import { TileFeaturesResponse } from "../types"; 4 | 5 | export default function tileFeaturesSpatialIndex(arg: { 6 | tiles: any; 7 | geometryToIntersect: Polygon | MultiPolygon; 8 | geoColumName: string; 9 | spatialIndex: SpatialIndex; 10 | }): TileFeaturesResponse; 11 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/InvalidColumnError.js: -------------------------------------------------------------------------------- 1 | const NAME = 'InvalidColumnError'; 2 | const ERR_START_MESSAGE = `${NAME}: `; 3 | 4 | export class InvalidColumnError extends Error { 5 | constructor(message) { 6 | super(`${ERR_START_MESSAGE}${message}`); 7 | this.name = NAME; 8 | } 9 | 10 | static is(error) { 11 | return ( 12 | error instanceof InvalidColumnError || error.message?.includes(ERR_START_MESSAGE) 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/ArrowDropIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function ArrowDropIcon(props) { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/ToggleButtonGroup.d.ts: -------------------------------------------------------------------------------- 1 | import { ToggleButtonGroupProps as MuiToggleButtonGroupProps } from '@mui/material'; 2 | 3 | export type ToggleButtonGroupProps = MuiToggleButtonGroupProps & { 4 | variant?: 'contained' | 'floating' | 'unbounded'; 5 | backgroundColor?: 'primary' | 'secondary' | 'transparent'; 6 | }; 7 | 8 | declare const ToggleButtonGroup: (props: ToggleButtonGroupProps) => JSX.Element; 9 | export default ToggleButtonGroup; 10 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/featureSelectionConstants.js: -------------------------------------------------------------------------------- 1 | // Don't rename values. These values come from nebula. 2 | export const FEATURE_SELECTION_MODES = Object.freeze({ 3 | POLYGON: 'DrawPolygonMode', 4 | RECTANGLE: 'DrawRectangleMode', 5 | CIRCLE: 'DrawCircleFromCenterMode', 6 | LASSO_TOOL: 'DrawPolygonByDraggingMode' 7 | }); 8 | 9 | export const EDIT_MODES = Object.freeze({ 10 | EDIT: 'edit' 11 | }); 12 | 13 | export const MASK_ID = 'feature_selection_mask'; 14 | -------------------------------------------------------------------------------- /packages/react-core/src/filters/FilterTypes.d.ts: -------------------------------------------------------------------------------- 1 | export enum FilterTypes { 2 | IN = 'in', 3 | BETWEEN = 'between', // [a, b] both are included 4 | CLOSED_OPEN = 'closed_open', // [a, b) a is included, b is not 5 | TIME = 'time', 6 | STRING_SEARCH = 'stringSearch' 7 | } 8 | 9 | export const filterFunctions: Record< 10 | string, 11 | ( 12 | filterValues: unknown[], 13 | featureValue: unknown, 14 | params?: Record 15 | ) => boolean 16 | >; 17 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/CursorIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function CursorIcon(props) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-api/src/index.d.ts: -------------------------------------------------------------------------------- 1 | export { executeSQL } from './api/SQL'; 2 | export { ldsGeocode } from './api/lds'; 3 | export { getStats as _getStats } from './api/stats'; 4 | export { getTileJson as _getTileJson } from './api/tilejson'; 5 | export { executeModel as _executeModel } from './api/model'; 6 | 7 | export { default as useCartoLayerProps } from './hooks/useCartoLayerProps'; 8 | 9 | export { Credentials, UseCartoLayerFilterProps, SourceProps, MAP_TYPES, API_VERSIONS } from './types'; 10 | -------------------------------------------------------------------------------- /packages/react-api/src/api/SQL.d.ts: -------------------------------------------------------------------------------- 1 | import { Credentials, ExecuteSQLResponse, QueryParameters } from '../types'; 2 | import { FeatureCollection } from 'geojson'; 3 | 4 | export function executeSQL({ 5 | credentials, 6 | query, 7 | connection, 8 | opts, 9 | queryParameters 10 | }: { 11 | credentials: Credentials; 12 | query: string; 13 | connection?: string; 14 | opts?: unknown; 15 | queryParameters?: QueryParameters; 16 | }): ExecuteSQLResponse; 17 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/groupByDate.d.ts: -------------------------------------------------------------------------------- 1 | import { GroupByFeature } from '../types'; 2 | import { GroupDateTypes } from './constants/GroupDateTypes'; 3 | import { AggregationTypes } from './constants/AggregationTypes'; 4 | 5 | export function groupValuesByDateColumn(args: { 6 | data: Record[]; 7 | valuesColumns?: string[]; 8 | joinOperation?: AggregationTypes; 9 | keysColumn?: string; 10 | groupType?: GroupDateTypes; 11 | operation?: AggregationTypes; 12 | }): GroupByFeature; 13 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/organisms/AppBar/BrandLogo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { styled } from '@mui/material/styles'; 3 | 4 | const Logo = styled('div')(({ theme }) => ({ 5 | display: 'flex', 6 | marginRight: theme.spacing(1.5), 7 | 8 | '& a': { 9 | display: 'flex' 10 | }, 11 | 12 | '& svg, & img': { 13 | width: theme.spacing(4), 14 | height: theme.spacing(4) 15 | } 16 | })); 17 | 18 | export default function BrandLogo({ logo }) { 19 | return {logo}; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react-ui/src/theme/themeConstants.js: -------------------------------------------------------------------------------- 1 | import { getSpacing } from './themeUtils'; 2 | 3 | // Common 4 | export const SPACING = 8; 5 | 6 | // Breakpoints 7 | export const BREAKPOINTS = { 8 | XS: 320, 9 | SM: 600, 10 | MD: 960, 11 | LG: 1280, 12 | XL: 1600 13 | }; 14 | 15 | // Icons 16 | export const ICON_SIZE_SMALL = getSpacing(1.5); 17 | export const ICON_SIZE_MEDIUM = getSpacing(2.25); 18 | export const ICON_SIZE_LARGE = getSpacing(3); 19 | 20 | // AppBar 21 | export const APPBAR_SIZE = getSpacing(6); 22 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/dot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /packages/react-core/__tests__/utils/randomString.test.js: -------------------------------------------------------------------------------- 1 | import { randomString } from '../../src/utils/randomString'; 2 | 3 | describe('randomString', () => { 4 | test('should return a string', () => { 5 | expect(typeof randomString(0) === 'string').toBe(true); 6 | }); 7 | 8 | test('should return an empty string', () => { 9 | expect(randomString(0)).toBe(''); 10 | }); 11 | 12 | test('should return a string of required length', () => { 13 | expect(randomString(100).length).toBe(100); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/react-api/src/index.js: -------------------------------------------------------------------------------- 1 | export { executeSQL } from './api/SQL'; 2 | export { ldsGeocode } from './api/lds'; 3 | export { getStats as _getStats } from './api/stats'; 4 | export { getTileJson as _getTileJson } from './api/tilejson'; 5 | export { executeModel as _executeModel } from './api/model'; 6 | 7 | export { default as useCartoLayerProps } from './hooks/useCartoLayerProps'; 8 | 9 | export { getDataFilterExtensionProps } from './hooks/dataFilterExtensionUtil'; 10 | 11 | export { MAP_TYPES, API_VERSIONS } from './types'; 12 | -------------------------------------------------------------------------------- /packages/react-core/src/filters/tileFeaturesGeometries.d.ts: -------------------------------------------------------------------------------- 1 | import { Polygon, MultiPolygon } from 'geojson'; 2 | import { TileFeaturesResponse } from '../types'; 3 | import { TILE_FORMATS } from '../types'; 4 | 5 | export default function tileFeaturesGeometries(arg: { 6 | tiles: any; 7 | tileFormat: typeof TILE_FORMATS; 8 | geometryToIntersect: Polygon | MultiPolygon; 9 | uniqueIdProperty?: string; 10 | options?: { storeGeometry: boolean }; 11 | }): TileFeaturesResponse; 12 | 13 | export const FEATURE_GEOM_PROPERTY: string; 14 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/AggregationTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for the different types of aggregations available for widgets 3 | * @enum {string} 4 | * @readonly 5 | */ 6 | export const AggregationTypes = Object.freeze({ 7 | /** Count */ 8 | COUNT: 'count', 9 | 10 | /** Average */ 11 | AVG: 'avg', 12 | 13 | /** Minimum */ 14 | MIN: 'min', 15 | 16 | /** Maximum */ 17 | MAX: 'max', 18 | 19 | /** Sum */ 20 | SUM: 'sum', 21 | 22 | /** Custom aggregation expression */ 23 | CUSTOM: 'custom' 24 | }); 25 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/carto-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /copy-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | echo "Missing target directory. Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | SOURCE_DIR="packages" 9 | TARGET_DIR="$1" 10 | 11 | for dir in "$SOURCE_DIR"/*; do 12 | if [ -d "$dir" ]; then 13 | DIR_NAME=$(basename "$dir") 14 | 15 | SOURCE_PATH="$dir/dist" 16 | TARGET_PATH="$TARGET_DIR/$DIR_NAME" 17 | 18 | echo "Copying $SOURCE_PATH to $TARGET_PATH" 19 | cp -r "$SOURCE_PATH" "$TARGET_PATH" 20 | fi 21 | done 22 | 23 | echo "All directories copied successfully!" -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/carto-logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/figma.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/AccordionGroup.d.ts: -------------------------------------------------------------------------------- 1 | export type AccordionGroupProps = React.HTMLAttributes & { 2 | variant?: 'standard' | 'outlined'; 3 | items: [ 4 | { 5 | summary: string; 6 | content: React.ReactNode; 7 | disabled?: boolean; 8 | defaultExpanded?: boolean; 9 | onChange?: (event: React.SyntheticEvent, expanded: boolean) => void; 10 | } 11 | ]; 12 | }; 13 | 14 | declare const AccordionGroup: (props: AccordionGroupProps) => JSX.Element; 15 | export default AccordionGroup; 16 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/Alert.d.ts: -------------------------------------------------------------------------------- 1 | import { AlertProps as MuiAlertProps } from '@mui/material/Alert'; 2 | 3 | export type AlertProps = Omit & { 4 | content?: 'inline' | 'block'; 5 | severity?: CartoAlertSeverity; 6 | open?: boolean; 7 | title?: React.ReactNode | string; 8 | isSticky?: boolean; 9 | }; 10 | 11 | export type CartoAlertSeverity = 'neutral' | 'info' | 'success' | 'warning' | 'error'; 12 | 13 | declare const Alert: (props: AlertProps) => JSX.Element; 14 | export default Alert; 15 | -------------------------------------------------------------------------------- /packages/react-workers/src/workerMethods.d.ts: -------------------------------------------------------------------------------- 1 | export enum Methods { 2 | TILE_FEATURES = 'tileFeatures', 3 | FEATURES_FORMULA = 'featuresFormula', 4 | FEATURES_HISTOGRAM = 'featuresHistogram', 5 | FEATURES_CATEGORY = 'featuresCategory', 6 | FEATURES_SCATTERPLOT = 'featuresScatterPlot', 7 | FEATURES_TIME_SERIES = 'featuresTimeSeries', 8 | FEATURES_RAW = 'featuresRawFeatures', 9 | FEATURES_RANGE = 'featuresRange', 10 | LOAD_GEOJSON_FEATURES = 'loadGeoJSONFeatures', 11 | GEOJSON_FEATURES = 'featuresGeoJSON', 12 | LOAD_TILES = 'loadTiles' 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-workers/src/workerMethods.js: -------------------------------------------------------------------------------- 1 | export const Methods = Object.freeze({ 2 | TILE_FEATURES: 'tileFeatures', 3 | FEATURES_FORMULA: 'featuresFormula', 4 | FEATURES_HISTOGRAM: 'featuresHistogram', 5 | FEATURES_CATEGORY: 'featuresCategory', 6 | FEATURES_SCATTERPLOT: 'featuresScatterPlot', 7 | FEATURES_TIME_SERIES: 'featuresTimeSeries', 8 | FEATURES_RAW: 'featuresRawFeatures', 9 | FEATURES_RANGE: 'featuresRange', 10 | LOAD_GEOJSON_FEATURES: 'loadGeoJSONFeatures', 11 | GEOJSON_FEATURES: 'featuresGeoJSON', 12 | LOAD_TILES: 'loadTiles' 13 | }); 14 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/constants/GroupDateTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Enum for the different types of group by date available for widgets 3 | * @enum {string} 4 | * @readonly 5 | */ 6 | export const GroupDateTypes = Object.freeze({ 7 | /** Years */ 8 | YEARS: 'year', 9 | 10 | /** Months */ 11 | MONTHS: 'month', 12 | 13 | /** Weeks */ 14 | WEEKS: 'week', 15 | 16 | /** Days */ 17 | DAYS: 'day', 18 | 19 | /** Hours */ 20 | HOURS: 'hour', 21 | 22 | /** Minutes */ 23 | MINUTES: 'minute', 24 | 25 | /** Seconds */ 26 | SECONDS: 'second' 27 | }); 28 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/columns.js: -------------------------------------------------------------------------------- 1 | import { SpatialIndex } from '../operations/constants/SpatialIndexTypes'; 2 | 3 | export function getColumnNameFromGeoColumn(geoColumn) { 4 | const parts = geoColumn.split(':'); 5 | return parts.length === 1 ? parts[0] : parts.length === 2 ? parts[1] : null; 6 | } 7 | 8 | export function getSpatialIndexFromGeoColumn(geoColumn) { 9 | const parts = geoColumn.split(':'); 10 | return (parts.length === 1 || parts.length === 2) && 11 | Object.values(SpatialIndex).includes(parts[0].toLowerCase()) 12 | ? parts[0].toLowerCase() 13 | : null; 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/UploadIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function UploadIcon(props) { 5 | return ( 6 | 7 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-api/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-api` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package to access CARTO APIs. 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-core/__tests__/utils/debounce.test.js: -------------------------------------------------------------------------------- 1 | import { debounce } from '../../src/utils/debounce'; 2 | 3 | describe('debounce', () => { 4 | beforeAll(() => { 5 | jest.useFakeTimers(); 6 | }); 7 | 8 | test('should be executed after timeout', () => { 9 | const TIMEOUT = 100; 10 | 11 | const mockedFunction = jest.fn(); 12 | const debouncedFn = debounce(mockedFunction, TIMEOUT); 13 | debouncedFn(); 14 | 15 | expect(mockedFunction).toHaveBeenCalledTimes(0); 16 | jest.advanceTimersByTime(TIMEOUT); 17 | expect(mockedFunction).toHaveBeenCalledTimes(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/react-auth/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | /^@carto\/react-.+$/, 11 | 'react', 12 | 'react-dom', 13 | ], 14 | output: { 15 | path: path.resolve(__dirname, './dist'), 16 | filename: 'index.js', 17 | library: 'cartoReactAuth', 18 | libraryTarget: 'umd' 19 | } 20 | }; 21 | 22 | module.exports = webpackConfig; 23 | -------------------------------------------------------------------------------- /packages/react-api/__tests__/mockReduxHooks.js: -------------------------------------------------------------------------------- 1 | import * as redux from 'react-redux'; 2 | 3 | const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); 4 | const useSelectorSpy = jest.spyOn(redux, 'useSelector'); 5 | 6 | export function mockReduxHooks(dispatchValue, selectorValue) { 7 | const mockDispatchFn = jest.fn(dispatchValue); 8 | useDispatchSpy.mockReturnValue(mockDispatchFn); 9 | 10 | const mockSelectorFn = jest.fn(selectorValue); 11 | useSelectorSpy.mockReturnValue(mockSelectorFn); 12 | } 13 | 14 | export function mockClear() { 15 | useDispatchSpy.mockClear(); 16 | useSelectorSpy.mockClear(); 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-auth/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-auth` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package to use CARTO OAuth system. 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | /^@carto\/react-.+$/, 11 | /^@deck.gl\/.+$/, 12 | '@reduxjs/toolkit' 13 | ], 14 | output: { 15 | path: path.resolve(__dirname, './dist'), 16 | filename: 'index.js', 17 | library: 'cartoReactRedux', 18 | libraryTarget: 'umd' 19 | } 20 | }; 21 | 22 | module.exports = webpackConfig; 23 | -------------------------------------------------------------------------------- /packages/react-ui/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-ui` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package with UI components, based on MaterialUI + a CARTO theme 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/development-tools/carto-for-react) 18 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/carto-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/react-core/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-core` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package with core elements, used by different packages 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/organisms/AppBar/BrandText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { styled, useTheme } from '@mui/material/styles'; 3 | 4 | import Typography from '../../atoms/Typography'; 5 | 6 | const Text = styled(Typography)({ 7 | display: 'flex', 8 | alignItems: 'center', 9 | whiteSpace: 'nowrap' 10 | }); 11 | 12 | export default function BrandText({ text }) { 13 | const theme = useTheme(); 14 | 15 | return ( 16 | 21 | {text} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/TableWidgetUI/Skeleton/TableSkeletonRow.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton, TableCell } from '@mui/material'; 3 | 4 | const TableSkeletonRow = ({ width, rows = 4, index }) => { 5 | function getSkeletonWidth(index) { 6 | const sizes = [72, 48, 96, 32, 64]; 7 | let chosenIndex = index % sizes.length; 8 | 9 | return sizes[chosenIndex]; 10 | } 11 | 12 | return [...Array(rows)].map((_, i) => ( 13 | 14 | 15 | 16 | )); 17 | }; 18 | 19 | export default TableSkeletonRow; 20 | -------------------------------------------------------------------------------- /packages/react-workers/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-workers` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package to use CARTO Workers system. 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-basemaps/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | /^@carto\/react-.+$/, 11 | /^@deck.gl\/.+$/, 12 | 'react', 13 | 'react-dom' 14 | ], 15 | output: { 16 | path: path.resolve(__dirname, './dist'), 17 | filename: 'index.js', 18 | library: 'cartoReactBasemaps', 19 | libraryTarget: 'umd' 20 | } 21 | }; 22 | 23 | module.exports = webpackConfig; 24 | -------------------------------------------------------------------------------- /packages/react-redux/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-redux` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package with CARTO utilities for a React + Redux project 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/useSkeleton.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | function useSkeleton(isDataLoading) { 4 | const [showSkeleton, setShowSkeleton] = useState(false); 5 | const [skeletonEverBeenShown, setSkeletonEverBeenShown] = useState(false); 6 | 7 | useEffect(() => { 8 | if (isDataLoading && !skeletonEverBeenShown) { 9 | setShowSkeleton(true); 10 | setSkeletonEverBeenShown(true); 11 | } 12 | 13 | if (!isDataLoading) { 14 | setShowSkeleton(false); 15 | } 16 | }, [isDataLoading, skeletonEverBeenShown]); 17 | 18 | return { showSkeleton }; 19 | } 20 | 21 | export default useSkeleton; 22 | -------------------------------------------------------------------------------- /docs/upgrade-guide.md: -------------------------------------------------------------------------------- 1 | # Upgrade Guide 2 | 3 | ## Upgrading from @carto/react v1.0 to v1.1 4 | 5 | ### executeSQL parameters via destructuring 6 | 7 | ExecuteSQL parameters are passed using object destructuring. 8 | 9 | ```javascript 10 | executeSQL({ credentials, query }); 11 | ``` 12 | 13 | ### Credentials apiVersion 14 | 15 | A new `apiVersion` parameter is included in cartoSlice to indicate the CARTO API to be used. 16 | 17 | ```javascript 18 | import { API_VERSIONS } from '@deck.gl/carto/typed'; 19 | 20 | export const initialState = { 21 | ... 22 | , 23 | credentials: { 24 | apiVersion: API_VERSIONS.V2, 25 | ... 26 | } 27 | ... 28 | } 29 | ``` 30 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/utils.d.ts: -------------------------------------------------------------------------------- 1 | import { SourceProps, MAP_TYPES } from '@carto/react-api'; 2 | import { FiltersLogicalOperators, Provider, _FilterTypes } from '@carto/react-core'; 3 | import { SourceFilters, ViewState } from '@carto/react-redux'; 4 | 5 | export function isRemoteCalculationSupported(prop: { source: SourceProps }): boolean; 6 | 7 | export function sourceAndFiltersToSQL(props: { 8 | data: string; 9 | filters?: SourceFilters; 10 | filtersLogicalOperator?: FiltersLogicalOperators; 11 | provider: Provider; 12 | type: typeof MAP_TYPES; 13 | }): string; 14 | 15 | export function getSqlEscapedSource(table: string, provider: Provider): string; 16 | -------------------------------------------------------------------------------- /packages/react-basemaps/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-basemaps` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package to use CARTO basemaps + an easy to use GoogleMap React component 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-api/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | /^@carto\/react-.+$/, 11 | /^@deck.gl\/.+$/, 12 | 'react', 13 | 'react-dom', 14 | 'react-redux', 15 | 'redux', 16 | ], 17 | output: { 18 | path: path.resolve(__dirname, './dist'), 19 | filename: 'index.js', 20 | library: 'cartoReactApi', 21 | libraryTarget: 'umd' 22 | } 23 | }; 24 | 25 | module.exports = webpackConfig; 26 | -------------------------------------------------------------------------------- /packages/react-core/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | // Be careful what you add as external because it can significantly affect the size of the worker package. 10 | externals: [/^@turf\/.+$/, 'h3-js', 'quadbin'], 11 | output: { 12 | path: path.resolve(__dirname, './dist'), 13 | filename: 'index.js', 14 | library: 'cartoReactCore', 15 | libraryTarget: 'umd' 16 | } 17 | }; 18 | 19 | module.exports = webpackConfig; 20 | -------------------------------------------------------------------------------- /packages/react-widgets/README.md: -------------------------------------------------------------------------------- 1 | # `@carto/react-widgets` 2 | 3 |

4 | 5 | version 6 | 7 | 8 | 9 | downloads 10 | 11 |

12 | 13 |
14 | 15 | Package to use CARTO UI widgets + advanced filters within a React + Redux project 16 | 17 | See the official doc & reference at [CARTO for React](https://docs.carto.com/carto-for-developers/carto-for-react/) 18 | -------------------------------------------------------------------------------- /packages/react-widgets/src/layers/MaskLayer.js: -------------------------------------------------------------------------------- 1 | import { useSelector } from 'react-redux'; 2 | import { SolidPolygonLayer } from '@deck.gl/layers'; 3 | import { MASK_ID } from '@carto/react-core/'; 4 | import { selectValidSpatialFilter } from '@carto/react-redux/'; 5 | 6 | export default function MaskLayer() { 7 | const spatialFilterGeometry = useSelector(selectValidSpatialFilter); 8 | const maskData = !!spatialFilterGeometry 9 | ? [{ polygon: spatialFilterGeometry?.geometry?.coordinates }] 10 | : []; 11 | 12 | return new SolidPolygonLayer({ 13 | id: MASK_ID, 14 | operation: 'mask', 15 | data: maskData, 16 | getFillColor: [255, 255, 255, 255] 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /packages/react-widgets/src/widgets/utils/sortLayers.js: -------------------------------------------------------------------------------- 1 | import { _assert as assert } from '@carto/react-core'; 2 | 3 | /** 4 | * Sorts a list of layers based on a list of id property values 5 | * @param {{ id: string }[]} layers - Array of layers to sort 6 | * @param {string[]} layerOrder - Array of ids to sort by 7 | * @returns sorted array 8 | */ 9 | export default function sortLayers(layers = [], layerOrder = []) { 10 | if (!layerOrder.length) { 11 | return layers; 12 | } 13 | return [...layers].sort((layerA, layerB) => { 14 | assert(layerA.id && layerB.id, 'Layer must have an id'); 15 | return layerOrder.indexOf(layerB.id) - layerOrder.indexOf(layerA.id); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/UploadField/UploadField.d.ts: -------------------------------------------------------------------------------- 1 | import { InputProps } from '@mui/material/Input'; 2 | import { TextFieldProps } from '@mui/material/TextField'; 3 | 4 | export type UploadFieldProps = Omit & { 5 | name?: string; 6 | buttonText?: string; 7 | accept?: string[] | string | null; 8 | files?: []; 9 | multiple?: boolean; 10 | onChange?: (file?: File | null) => void; 11 | inProgress?: boolean; 12 | InputProps?: Partial; 13 | nativeInputProps?: object; 14 | placeholder?: string | React.ReactNode; 15 | }; 16 | 17 | declare const UploadField: (props: UploadFieldProps) => JSX.Element; 18 | export default UploadField; 19 | -------------------------------------------------------------------------------- /packages/react-api/__tests__/mockSqlApiRequest.js: -------------------------------------------------------------------------------- 1 | const _global = typeof global !== 'undefined' ? global : window; 2 | 3 | export function mockSqlApiRequest({ 4 | response, 5 | responseIsOk = true, 6 | status = 200, 7 | query, 8 | credentials 9 | }) { 10 | _global.fetch = jest.fn(() => 11 | Promise.resolve({ 12 | json: () => Promise.resolve(response), 13 | ok: responseIsOk, 14 | status 15 | }) 16 | ); 17 | 18 | _global.Request = jest.fn( 19 | () => 20 | `https://public.carto.com/api/v2/sql?api_key=${credentials.apiKey}&client=${credentials.username}&q=${query}` 21 | ); 22 | } 23 | 24 | export function mockClear() { 25 | fetch.mockClear(); 26 | Request.mockClear(); 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/utils/formatterUtils.test.js: -------------------------------------------------------------------------------- 1 | import { processFormatterRes } from '../../../src/widgets/utils/formatterUtils'; 2 | 3 | describe('Formatter utils', () => { 4 | describe('processFormatterRes', () => { 5 | test('if formatterRes is an object', () => { 6 | const formatterRes = { prefix: '$', value: 5 }; 7 | expect(processFormatterRes(formatterRes)).toBe('$5'); 8 | const formatterRes2 = { suffix: '$', value: 5 }; 9 | expect(processFormatterRes(formatterRes2)).toBe('5$'); 10 | }); 11 | test("if formatterRes isn't an object", () => { 12 | const formatterRes = '$5'; 13 | expect(processFormatterRes(formatterRes)).toBe('$5'); 14 | }); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/Button.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import { Button as MuiButton } from '@mui/material'; 3 | import Typography from './Typography'; 4 | 5 | const Button = forwardRef(({ children, ...otherProps }, ref) => { 6 | // forwardRef needed to be able to hold a reference, in this way it can be a child for some Mui components, like Tooltip 7 | // https://mui.com/material-ui/guides/composition/#caveat-with-refs 8 | 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | }); 17 | 18 | export default Button; 19 | -------------------------------------------------------------------------------- /packages/react-widgets/__tests__/mockSqlApiRequest.js: -------------------------------------------------------------------------------- 1 | const _global = typeof global !== 'undefined' ? global : window; 2 | 3 | export function mockSqlApiRequest({ 4 | response, 5 | responseIsOk = true, 6 | status = 200, 7 | sql, 8 | credentials 9 | }) { 10 | _global.fetch = jest.fn(() => 11 | Promise.resolve({ 12 | json: () => Promise.resolve(response), 13 | ok: responseIsOk, 14 | status 15 | }) 16 | ); 17 | 18 | _global.Request = jest.fn( 19 | () => 20 | `https://public.carto.com/api/v2/sql?api_key=${credentials.apiKey}&client=${credentials.username}&q=${sql}` 21 | ); 22 | } 23 | 24 | export function mockClear() { 25 | fetch.mockClear(); 26 | Request.mockClear(); 27 | } 28 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/SelectField.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SelectProps } from '@mui/material/Select'; 3 | import { MenuProps } from '@mui/material/Menu'; 4 | import { InputProps } from '@mui/material/Input'; 5 | 6 | export type SelectFieldProps = Omit< 7 | SelectProps, 8 | 'placeholder' 9 | > & { 10 | placeholder?: React.ReactNode | string; 11 | size?: 'small' | 'medium'; 12 | menuProps?: Partial; 13 | inputProps?: Partial; 14 | helperText?: React.ReactNode | string; 15 | labelSecondary?: React.ReactNode; 16 | }; 17 | 18 | declare const SelectField: (props: SelectFieldProps) => JSX.Element; 19 | export default SelectField; 20 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/SearchIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function SearchIcon(props) { 5 | return ( 6 | 7 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/react-widgets/__tests__/mockReduxHooks.js: -------------------------------------------------------------------------------- 1 | import * as redux from 'react-redux'; 2 | 3 | const useDispatchSpy = jest.spyOn(redux, 'useDispatch'); 4 | const useSelectorSpy = jest.spyOn(redux, 'useSelector'); 5 | 6 | export function mockReduxHooks(dispatchValue, selectorValue) { 7 | const mockDispatchFn = jest.fn(dispatchValue); 8 | useDispatchSpy.mockReturnValue(mockDispatchFn); 9 | 10 | const mockSelectorFn = jest.fn(selectorValue); 11 | useSelectorSpy.mockReturnValue(mockSelectorFn); 12 | } 13 | 14 | export function mockSetup() { 15 | return { useDispatch: useDispatchSpy, useSelector: useSelectorSpy }; 16 | } 17 | 18 | export function mockClear() { 19 | useDispatchSpy.mockClear(); 20 | useSelectorSpy.mockClear(); 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-ui/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | '@carto/react-core', 11 | /^@mui\/.+$/, 12 | /^@emotion\/.+$/, 13 | /^@formatjs\/.+$/, 14 | 'echarts', 15 | 'echarts-for-react', 16 | 'react', 17 | 'react-dom', 18 | 'react-intl' 19 | ], 20 | output: { 21 | path: path.resolve(__dirname, './dist'), 22 | filename: 'index.js', 23 | library: 'cartoReactUi', 24 | libraryTarget: 'umd' 25 | } 26 | }; 27 | 28 | module.exports = webpackConfig; 29 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/scatterPlot.js: -------------------------------------------------------------------------------- 1 | import { aggregate } from './aggregation'; 2 | 3 | // Filters invalid features and formats data 4 | export function scatterPlot({ 5 | data, 6 | xAxisColumns, 7 | xAxisJoinOperation, 8 | yAxisColumns, 9 | yAxisJoinOperation 10 | }) { 11 | return data.reduce((acc, feature) => { 12 | const xValue = aggregate(feature, xAxisColumns, xAxisJoinOperation); 13 | const xIsValid = xValue !== null && xValue !== undefined; 14 | const yValue = aggregate(feature, yAxisColumns, yAxisJoinOperation); 15 | const yIsValid = yValue !== null && yValue !== undefined; 16 | 17 | if (xIsValid && yIsValid) { 18 | acc.push([xValue, yValue]); 19 | } 20 | 21 | return acc; 22 | }, []); 23 | } 24 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/index.js: -------------------------------------------------------------------------------- 1 | import ArrowDropIcon from './icons/ArrowDropIcon'; 2 | import CursorIcon from './icons/CursorIcon'; 3 | import CircleIcon from './icons/CircleIcon'; 4 | import LassoIcon from './icons/LassoIcon'; 5 | import LayerIcon from './icons/LayerIcon'; 6 | import PolygonIcon from './icons/PolygonIcon'; 7 | import RectangleIcon from './icons/RectangleIcon'; 8 | import UploadIcon from './icons/UploadIcon'; 9 | import SearchIcon from './icons/SearchIcon'; 10 | 11 | export const icons = { 12 | ArrowDrop: ArrowDropIcon, 13 | Circle: CircleIcon, 14 | Cursor: CursorIcon, 15 | Lasso: LassoIcon, 16 | Layer: LayerIcon, 17 | Polygon: PolygonIcon, 18 | Rectangle: RectangleIcon, 19 | Upload: UploadIcon, 20 | Search: SearchIcon 21 | }; 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/RectangleIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function RectangleIcon(props) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-widgets/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | /^@carto\/react-.+$/, 11 | /^@deck.gl\/.+$/, 12 | /^@mui\/.+$/, 13 | /^@emotion\/.+$/, 14 | /^@deck.gl-community\/.+$/, 15 | 'react', 16 | 'react-dom', 17 | 'react-redux', 18 | 'redux' 19 | ], 20 | output: { 21 | path: path.resolve(__dirname, './dist'), 22 | filename: 'index.js', 23 | library: 'cartoReactWidgets', 24 | libraryTarget: 'umd' 25 | } 26 | }; 27 | 28 | module.exports = webpackConfig; 29 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/comparative/ComparativeFormulaWidgetUI/FormulaLabel.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, useTheme } from '@mui/material'; 3 | import { styled } from '@mui/material/styles'; 4 | 5 | import Typography from '../../../components/atoms/Typography'; 6 | 7 | const Note = styled(Typography)(({ theme }) => ({ 8 | display: 'inline-block', 9 | marginTop: theme.spacing(0.5) 10 | })); 11 | 12 | function FormulaLabel({ row }) { 13 | const theme = useTheme(); 14 | 15 | const { label, color } = row; 16 | 17 | return label ? ( 18 | 19 | 20 | {label} 21 | 22 | 23 | ) : null; 24 | } 25 | 26 | export default FormulaLabel; 27 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/UploadField/UploadFieldBase.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TextFieldProps } from '@mui/material/TextField'; 3 | import { InputProps } from '@mui/material/Input'; 4 | 5 | export type UploadFieldBaseProps = Omit & { 6 | name?: string; 7 | multiple?: boolean; 8 | handleReset?: () => void; 9 | handleOpen?: () => void; 10 | dragOver?: boolean; 11 | placeholder?: string | React.ReactNode; 12 | buttonText?: string; 13 | inProgress?: boolean; 14 | InputProps?: Partial; 15 | size?: 'small' | 'medium'; 16 | hasFiles?: boolean; 17 | cursor?: 'pointer' | 'default'; 18 | }; 19 | 20 | declare const UploadFieldBase: (props: UploadFieldBaseProps) => JSX.Element; 21 | export default UploadFieldBase; 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/organisms/AppBar/SecondaryText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { styled, useTheme } from '@mui/material/styles'; 3 | 4 | import Typography from '../../atoms/Typography'; 5 | 6 | const Text = styled(Typography)(({ theme }) => ({ 7 | display: 'flex', 8 | alignItems: 'center', 9 | 10 | '&::before': { 11 | content: '"/"', 12 | margin: theme.spacing(0, 1), 13 | opacity: 0.6, 14 | color: theme.palette.brand.appBarContrastText 15 | } 16 | })); 17 | 18 | export default function SecondaryText({ text }) { 19 | const theme = useTheme(); 20 | 21 | return ( 22 | 28 | {text} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/LayerIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function LayerIcon(props) { 5 | return ( 6 | 7 | 11 | 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/PolygonIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function PolygonIcon(props) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/organisms/AppBar/AppBar.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { AppBarTypeMap as MuiAppBarTypeMap } from '@mui/material/AppBar'; 3 | import { OverridableComponent, OverrideProps } from '@mui/material/OverridableComponent'; 4 | 5 | export type AppBarTypeMap = 'header'> = MuiAppBarTypeMap< 6 | { 7 | brandLogo?: React.ReactNode; 8 | brandText?: React.ReactNode; 9 | secondaryText?: React.ReactNode; 10 | onClickMenu?: (event: React.MouseEvent) => void; 11 | showBurgerMenu?: boolean; 12 | }, 13 | D 14 | >; 15 | 16 | export type AppBarProps = 17 | OverrideProps, D>; 18 | 19 | declare const AppBar: OverridableComponent; 20 | 21 | export default AppBar; 22 | -------------------------------------------------------------------------------- /packages/react-ui/src/localization/localeUtils.js: -------------------------------------------------------------------------------- 1 | import { match } from '@formatjs/intl-localematcher'; 2 | 3 | export const DEFAULT_LOCALE = 'en'; 4 | 5 | export function flattenMessages(nestedMessages, prefix = '') { 6 | return Object.keys(nestedMessages).reduce((messages, key) => { 7 | const value = nestedMessages[key]; 8 | const prefixedKey = prefix ? `${prefix}.${key}` : key; 9 | if (typeof value === 'string') { 10 | messages[prefixedKey] = value; 11 | } else { 12 | Object.assign(messages, flattenMessages(value, prefixedKey)); 13 | } 14 | return messages; 15 | }, {}); 16 | } 17 | 18 | export function findMatchingMessagesLocale(locale, messages) { 19 | const localeMatcher = match([locale], Object.keys(messages), DEFAULT_LOCALE); 20 | return localeMatcher ? localeMatcher : DEFAULT_LOCALE; 21 | } 22 | -------------------------------------------------------------------------------- /packages/react-workers/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | // Path and filename of your result bundle. 4 | // Webpack will bundle all JavaScript into this file 5 | const webpackBaseConfig = require('../../webpack.base'); 6 | 7 | const webpackConfig = { 8 | ...webpackBaseConfig, 9 | externals: [ 10 | 'react', 11 | 'react-dom', 12 | ], 13 | output: { 14 | publicPath: '', 15 | path: path.resolve(__dirname, './dist'), 16 | filename: 'index.js', 17 | library: 'cartoReactWorkers', 18 | libraryTarget: 'umd' 19 | } 20 | }; 21 | 22 | webpackConfig.module.rules.push( 23 | { 24 | test: /\.worker\.js$/, 25 | exclude: /(node_modules)/, 26 | loader: 'worker-loader', 27 | options: { 28 | inline: 'fallback' 29 | } 30 | } 31 | ) 32 | 33 | module.exports = webpackConfig; 34 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/utils/detectTouchScreen.js: -------------------------------------------------------------------------------- 1 | // https://patrickhlauke.github.io/touch/touchscreen-detection/ 2 | export default function detectTouchscreen() { 3 | var result = false; 4 | if (window.PointerEvent && 'maxTouchPoints' in navigator) { 5 | // if Pointer Events are supported, just check maxTouchPoints 6 | if (navigator.maxTouchPoints > 0) { 7 | result = true; 8 | } 9 | } else { 10 | // no Pointer Events... 11 | if (window.matchMedia && window.matchMedia('(any-pointer:coarse)').matches) { 12 | // check for any-pointer:coarse which mostly means touchscreen 13 | result = true; 14 | } else if (window.TouchEvent || 'ontouchstart' in window) { 15 | // last resort - check for exposed touch events API / event handler 16 | result = true; 17 | } 18 | } 19 | return result; 20 | } 21 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/BarWidgetUI/BarSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Skeleton } from '@mui/material'; 3 | import { SkeletonBarsGrid, SkeletonBarItem, SKELETON_HEIGHT } from '../SkeletonWidgets'; 4 | 5 | const BarSkeleton = ({ height }) => { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default BarSkeleton; 24 | -------------------------------------------------------------------------------- /packages/react-widgets/__tests__/layers/FeatureSelectionLayer.test.js: -------------------------------------------------------------------------------- 1 | import { renderHook } from '@testing-library/react-hooks'; 2 | import { EventManager } from 'mjolnir.js'; 3 | import FeatureSelectionLayer from '../../src/layers/FeatureSelectionLayer'; 4 | import { mockReduxHooks } from '../mockReduxHooks'; 5 | 6 | describe('FeatureSelectionLayer', () => { 7 | mockReduxHooks(); 8 | describe('Google Maps Raster compatibility', () => { 9 | test('billboard property must be false', () => { 10 | const eventManager = new EventManager(document.createElement('div'), {}); 11 | // @ts-ignore 12 | const { result: layers } = renderHook(() => 13 | FeatureSelectionLayer({ mask: true, eventManager }) 14 | ); 15 | expect( 16 | layers.current.find((l) => l.id === 'FeatureSelectionLayer').props.billboard 17 | ).toBe(false); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/react-core/src/filters/geojsonFeatures.js: -------------------------------------------------------------------------------- 1 | import intersects from '@turf/boolean-intersects'; 2 | import { getGeometryToIntersect } from '../utils/geo'; 3 | 4 | export function geojsonFeatures({ geojson, viewport, geometry, uniqueIdProperty }) { 5 | let uniqueIdx = 0; 6 | // Map is used to cache multi geometries. Only a sucessfull intersect by multipolygon 7 | const map = new Map(); 8 | const geometryToIntersect = getGeometryToIntersect(viewport, geometry); 9 | 10 | if (!geometryToIntersect) { 11 | return []; 12 | } 13 | 14 | for (const feature of geojson.features) { 15 | const uniqueId = uniqueIdProperty 16 | ? feature.properties[uniqueIdProperty] 17 | : ++uniqueIdx; 18 | if (!map.has(uniqueId) && intersects(geometryToIntersect, feature)) { 19 | map.set(uniqueId, feature.properties); 20 | } 21 | } 22 | 23 | return Array.from(map.values()); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/organisms/ListItemGuide.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # ListItem 6 | 7 | In order to be interactive, you need to use `MenuItem` component, or use `ListItemButton`instead. 8 | 9 | ``` 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ``` 19 | 20 | ``` 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ``` 30 | 31 | To render a link: 32 | 33 | ``` 34 | 35 | 36 | 37 | ``` 38 | 39 | Note that `` is deprecated. 40 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/utils/docStyles.css: -------------------------------------------------------------------------------- 1 | .sbdocs .link-list { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | grid-template-rows: auto; 5 | gap: 16px; 6 | margin: 40px 0; 7 | } 8 | 9 | @media (min-width: 768px) { 10 | .sbdocs .link-list { 11 | grid-template-columns: 1fr 1fr; 12 | gap: 24px; 13 | } 14 | } 15 | 16 | .sbdocs .link-item { 17 | display: flex; 18 | align-items: flex-start; 19 | padding: 24px; 20 | color: #2c3032; 21 | border: 1px solid #2c30321f; 22 | border-radius: 6px; 23 | } 24 | .sbdocs .link-item img { 25 | width: 32px; 26 | height: 32px; 27 | margin-right: 16px; 28 | } 29 | .sbdocs .link-item strong { 30 | display: block; 31 | margin-bottom: 4px; 32 | } 33 | 34 | .sbdocs .text-tag { 35 | display: inline-block; 36 | padding: 0 8px; 37 | font-weight: 600; 38 | color: #47db99; 39 | border: 1px solid #47db99; 40 | border-radius: 6px; 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-core/src/filters/tileFeatures.js: -------------------------------------------------------------------------------- 1 | import { getGeometryToIntersect } from '../utils/geo'; 2 | import tileFeaturesGeometries from './tileFeaturesGeometries'; 3 | import tileFeaturesSpatialIndex from './tileFeaturesSpatialIndex'; 4 | 5 | export function tileFeatures({ 6 | tiles, 7 | viewport, 8 | geometry, 9 | uniqueIdProperty, 10 | tileFormat, 11 | geoColumName, 12 | spatialIndex, 13 | options 14 | }) { 15 | const geometryToIntersect = getGeometryToIntersect(viewport, geometry); 16 | 17 | if (!geometryToIntersect) { 18 | return []; 19 | } 20 | 21 | if (spatialIndex) { 22 | return tileFeaturesSpatialIndex({ 23 | tiles, 24 | geometryToIntersect, 25 | geoColumName, 26 | spatialIndex 27 | }); 28 | } 29 | return tileFeaturesGeometries({ 30 | tiles, 31 | tileFormat, 32 | geometryToIntersect, 33 | uniqueIdProperty, 34 | options 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/dateUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns midnight (local time) on the Monday preceeding a given date, in 3 | * milliseconds since the UNIX epoch. 4 | */ 5 | export function getMonday(date) { 6 | const dateCp = new Date(date); 7 | const day = dateCp.getDay(); 8 | const diff = dateCp.getDate() - day + (day ? 1 : -6); // adjust when day is sunday 9 | dateCp.setDate(diff); 10 | dateCp.setHours(0, 0, 0, 0); 11 | return dateCp.getTime(); 12 | } 13 | 14 | /** 15 | * Returns midnight (UTC) on the Monday preceeding a given date, in 16 | * milliseconds since the UNIX epoch. 17 | */ 18 | export function getUTCMonday(date) { 19 | const dateCp = new Date(date); 20 | const day = dateCp.getUTCDay(); 21 | const diff = dateCp.getUTCDate() - day + (day ? 1 : -6); // adjust when day is sunday 22 | dateCp.setUTCDate(diff); 23 | return Date.UTC(dateCp.getUTCFullYear(), dateCp.getUTCMonth(), dateCp.getUTCDate()); 24 | } 25 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/PieWidgetUI.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '../widgets/utils/testUtils'; 3 | import PieWidgetUI from '../../src/widgets/PieWidgetUI/PieWidgetUI'; 4 | import { mockEcharts } from './testUtils'; 5 | 6 | describe('PieWidgetUI', () => { 7 | beforeAll(() => { 8 | mockEcharts.init(); 9 | }); 10 | 11 | afterAll(() => { 12 | mockEcharts.destroy(); 13 | }); 14 | 15 | const DATA = [ 16 | { 17 | name: 'Category 1', 18 | value: 1 19 | }, 20 | { 21 | name: 'Category 2', 22 | value: 1 23 | } 24 | ]; 25 | 26 | const Widget = (props) => ( 27 | {}} {...props} /> 28 | ); 29 | 30 | test('renders correctly', () => { 31 | render(); 32 | }); 33 | 34 | test('with selected categories', () => { 35 | render(); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/UploadField/StyledUploadField.js: -------------------------------------------------------------------------------- 1 | import { TextField } from '@mui/material'; 2 | import { styled } from '@mui/material/styles'; 3 | 4 | const StyledUploadField = styled(TextField, { 5 | shouldForwardProp: (prop) => prop !== 'cursor' 6 | })(({ cursor, theme }) => ({ 7 | '&.MuiTextField-root .MuiInputBase-root': { 8 | cursor: cursor, 9 | paddingRight: theme.spacing(1), 10 | 11 | '& input': { 12 | cursor: cursor 13 | }, 14 | '&.Mui-disabled': { 15 | pointerEvents: 'none', 16 | 17 | '& .MuiButtonBase-root': { 18 | color: theme.palette.text.disabled 19 | } 20 | }, 21 | '&.MuiInputBase-sizeSmall': { 22 | paddingRight: theme.spacing(0.5) 23 | } 24 | }, 25 | '& .MuiFormLabel-root': { 26 | cursor: cursor, 27 | 28 | '&.Mui-disabled': { 29 | pointerEvents: 'none' 30 | } 31 | } 32 | })); 33 | 34 | export default StyledUploadField; 35 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/mui.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/legend/LegendProportion.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LegendProportion from '../../../../src/widgets/legend/legend-types/LegendProportion'; 3 | import { IntlProvider } from 'react-intl'; 4 | 5 | const DEFAULT_LEGEND = { 6 | legend: { 7 | labels: [0, 200] 8 | } 9 | }; 10 | 11 | const options = { 12 | title: 'Widgets/Legends/LegendProportion', 13 | component: LegendProportion, 14 | argTypes: { 15 | legend: {} 16 | }, 17 | parameters: { 18 | docs: { 19 | source: { 20 | type: 'auto' 21 | } 22 | } 23 | } 24 | }; 25 | 26 | export default options; 27 | 28 | const Template = (args) => { 29 | return ( 30 | 31 | 32 | 33 | ); 34 | }; 35 | 36 | export const Default = Template.bind({}); 37 | const DefaultProps = { ...DEFAULT_LEGEND }; 38 | Default.args = DefaultProps; 39 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/foundations/BreakpointsGuide.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # Breakpoints 6 | 7 | These are the keys available: 8 | 9 | ```js 10 | xs: 320; 11 | sm: 600; 12 | md: 960; 13 | lg: 1280; 14 | xl: 1600; 15 | ``` 16 | 17 | Examples of use and its result: 18 | 19 | ```css 20 | breakpoints.between('xs', 'sm') -> (min-width:320px) and (max-width:599.95px) 21 | breakpoints.between('sm', 'md') -> (min-width:600px) and (max-width:959.95px) 22 | breakpoints.between('md', 'lg') -> (min-width:960px) and (max-width:1279.95px) 23 | breakpoints.between('lg', 'xl') -> (min-width:1280px) and (max-width:1599.95px) 24 | 25 | breakpoints.only('xs') -> (min-width:320px) and (max-width:599.95px) 26 | 27 | breakpoints.up('xs') -> (min-width:320px) 28 | breakpoints.down('xs') -> (max-width:319.95px) 29 | 30 | breakpoints.not('xs') -> (min-width:600px) 31 | ``` 32 | -------------------------------------------------------------------------------- /webpack.base.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { ProgressPlugin } = require('webpack'); 3 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 4 | const mode = process.env.NODE_ENV || 'development'; 5 | 6 | // This is main configuration object. 7 | // Here you write different options and tell Webpack what to do 8 | module.exports = { 9 | // Default mode for Webpack is production. 10 | // Depending on mode Webpack will apply different things 11 | // on final bundle. 12 | mode, 13 | devtool: mode === 'development' ? 'eval-source-map' : 'source-map', 14 | entry: './src/index.js', 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.jsx?$/, 19 | exclude: [/(node_modules)/, /(dist)/], 20 | loader: 'babel-loader' 21 | } 22 | ] 23 | }, 24 | plugins: [ 25 | new ProgressPlugin() 26 | // Uncomment for bundle analysis 27 | // new BundleAnalyzerPlugin() 28 | ] 29 | }; 30 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/featureFlags.js: -------------------------------------------------------------------------------- 1 | export const Flags = Object.freeze({ 2 | REMOTE_WIDGETS: '2023-remote-widgets' 3 | }); 4 | 5 | // Default flags 6 | let featureFlags = [Flags.REMOTE_WIDGETS]; 7 | 8 | export function setFlags(flags) { 9 | const isValidFlag = (f) => typeof f === 'string' && f; 10 | 11 | if (Array.isArray(flags) && flags.every(isValidFlag)) { 12 | featureFlags = flags; 13 | } else if ( 14 | !Array.isArray(flags) && 15 | typeof flags === 'object' && 16 | Object.keys(flags).every(isValidFlag) 17 | ) { 18 | featureFlags = []; 19 | for (const [flag, value] of Object.entries(flags)) { 20 | if (value) { 21 | featureFlags.push(flag); 22 | } 23 | } 24 | } else { 25 | throw new Error(`Invalid feature flags: ${flags}`); 26 | } 27 | } 28 | 29 | export function clearFlags() { 30 | featureFlags = []; 31 | } 32 | 33 | export function hasFlag(flag) { 34 | return featureFlags.includes(flag); 35 | } 36 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/ScatterPlotWidget.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '../widgets/utils/testUtils'; 3 | import ScatterPlotWidgetUI from '../../src/widgets/ScatterPlotWidgetUI/ScatterPlotWidgetUI'; 4 | import { mockEcharts } from './testUtils'; 5 | 6 | describe('ScatterPlotWidgetUI', () => { 7 | beforeAll(() => { 8 | mockEcharts.init(); 9 | }); 10 | 11 | afterAll(() => { 12 | mockEcharts.destroy(); 13 | }); 14 | const DATA = [ 15 | [1, 2], 16 | [2, 4], 17 | [3, 6] 18 | ]; 19 | const Widget = (props) => ( 20 | {}} {...props} /> 21 | ); 22 | 23 | test('renders correctly', () => { 24 | render(); 25 | }); 26 | 27 | test('re-render with different data', () => { 28 | const { rerender } = render(); 29 | 30 | rerender(); 31 | rerender(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/RangeModel.js: -------------------------------------------------------------------------------- 1 | import { _executeModel } from '@carto/react-api'; 2 | import { Methods, executeTask } from '@carto/react-workers'; 3 | import { normalizeObjectKeys, wrapModelCall } from './utils'; 4 | 5 | export function getRange(props) { 6 | return wrapModelCall(props, fromLocal, fromRemote); 7 | } 8 | 9 | // From local 10 | function fromLocal(props) { 11 | const { source, column } = props; 12 | 13 | return executeTask(source.id, Methods.FEATURES_RANGE, { 14 | filters: source.filters, 15 | filtersLogicalOperator: source.filtersLogicalOperator, 16 | column 17 | }); 18 | } 19 | 20 | // From remote 21 | function fromRemote(props) { 22 | const { source, abortController, ...params } = props; 23 | const { column } = params; 24 | 25 | return _executeModel({ 26 | model: 'range', 27 | source, 28 | params: { column }, 29 | opts: { abortController } 30 | }).then((res) => normalizeObjectKeys(res.rows[0])); 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "jsx": "react", 5 | "strict": true, 6 | "noImplicitAny": false, 7 | "allowJs": true, 8 | "checkJs": false, 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "noEmit": true, 13 | "baseUrl": ".", 14 | // "compileOnSave": true, 15 | "paths": { 16 | "@carto/react-api/*": ["packages/react-api/src/*"], 17 | "@carto/react-auth/*": ["packages/react-auth/src/*"], 18 | "@carto/react-basemaps/*": ["packages/react-basemaps/src/*"], 19 | "@carto/react-core/*": ["packages/react-core/src/*"], 20 | "@carto/react-ui/*": ["packages/react-ui/src/*"], 21 | "@carto/react-redux/*": ["packages/react-redux/src/*"], 22 | "@carto/react-widgets/*": ["packages/react-widgets/src/*"], 23 | "@carto/react-workers/*": ["packages/react-workers/src/*"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/react-widgets/src/widgets/utils/validateCoordinates.js: -------------------------------------------------------------------------------- 1 | export const isLatitude = (lat) => { 2 | return isFinite(lat) && Math.abs(lat) <= 90; 3 | }; 4 | 5 | export const isLongitude = (lng) => { 6 | return isFinite(lng) && Math.abs(lng) <= 180; 7 | }; 8 | 9 | export const isCoordinate = (str) => { 10 | const coordinateRegexp = 11 | /^-?([1-8]?\d(\.\d{1,16})?|90(\.0{1,16})?)(\s*,?\s*)-?((1?[0-7]|\d)?\d(\.\d{1,16})?|180(\.0{1,16})?)$/; 12 | return coordinateRegexp.test(str); 13 | }; 14 | 15 | export const validateAndGenerateCoordsResult = (searchText) => { 16 | const [latitude, longitude] = 17 | searchText.indexOf(',') !== -1 ? searchText.split(',') : searchText.split(' '); 18 | const hasCoords = latitude && longitude; 19 | const areValidCoords = isLatitude(latitude) && isLongitude(longitude); 20 | if (!hasCoords || !areValidCoords) return false; 21 | return { 22 | longitude: parseFloat(longitude), 23 | latitude: parseFloat(latitude) 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/react-redux/__tests__/mockReducerManager.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | 3 | let mockedStore = {}; 4 | 5 | function mockReducerManagerCreation(initialReducers) { 6 | const reducers = { ...initialReducers }; 7 | let combinedReducer = Object.keys(reducers).length 8 | ? combineReducers(reducers) 9 | : () => {}; 10 | 11 | return { 12 | reduce: (state, action) => { 13 | return combinedReducer(state, action); 14 | }, 15 | add: (key, reducer) => { 16 | reducers[key] = reducer; 17 | combinedReducer = combineReducers(reducers); 18 | mockedStore.replaceReducer(combinedReducer); 19 | } 20 | }; 21 | } 22 | 23 | export function mockAppStoreConfiguration() { 24 | const reducerManager = mockReducerManagerCreation(); 25 | mockedStore = configureStore({ 26 | reducer: reducerManager.reduce 27 | }); 28 | 29 | mockedStore.reducerManager = reducerManager; 30 | 31 | return mockedStore; 32 | } 33 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/images/GraphLine.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function GraphLine(props) { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/LassoIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function LassoIcon(props) { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/react-widgets/src/hooks/useWidgetFilterValues.js: -------------------------------------------------------------------------------- 1 | import { selectSourceById } from '@carto/react-redux/'; 2 | import { useSelector } from 'react-redux'; 3 | import { useMemo } from 'react'; 4 | 5 | /** 6 | * Obtain widget's filter values. 7 | * 8 | * @param {object} props 9 | * @param {string} props.dataSource - ID of the source to get the filters from. 10 | * @param {string} props.id - ID of the widget that applied the filter. 11 | * @param {string=} props.column - name of column of this widget. 12 | * @param {string} props.type - type of filter 13 | */ 14 | export function useWidgetFilterValues({ dataSource, id, column, type }) { 15 | const { filters } = useSelector((state) => selectSourceById(state, dataSource) || {}); 16 | 17 | return useMemo(() => { 18 | if (!column) return null; 19 | const filter = filters?.[column]?.[type]; 20 | if (!filter || filter.owner !== id) { 21 | return null; 22 | } 23 | return filter.values; 24 | }, [filters, column, type, id]); 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/MultipleSelectField/MultipleSelectField.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SelectFieldProps } from '../../atoms/SelectField'; 3 | import { TooltipProps } from '@mui/material'; 4 | 5 | type MultipleSelectFieldOption = { 6 | label: string | React.ReactNode; 7 | value: string | number; 8 | disabled?: boolean; 9 | tooltip?: string | false; 10 | }; 11 | 12 | export type MultipleSelectFieldProps = Omit< 13 | SelectFieldProps, 14 | 'onChange' | 'defaultValue' | 'value' 15 | > & { 16 | options: MultipleSelectFieldOption[]; 17 | selectedOptions?: string[]; 18 | selectAllDisabled?: boolean; 19 | onChange: (options: string[]) => void; 20 | showCounter?: boolean; 21 | showFilters?: boolean; 22 | value?: string[] | string; 23 | tooltipPlacement?: TooltipProps['placement']; 24 | }; 25 | 26 | declare const MultipleSelectField: ( 27 | props: MultipleSelectFieldProps 28 | ) => JSX.Element; 29 | export default MultipleSelectField; 30 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Shortcut: (link?) 4 | 5 | Please include a summary of the change. If required, also include relevant info about motivation and context... And if PR includes UI elements (or if it helps for understanding it), please add extra files: snapshots, videos... 6 | 7 | If this PR requires a companion in carto-react-template, please also link them both. 8 | 9 | ## Type of change 10 | 11 | (choose one and remove the others) 12 | 13 | - Fix 14 | - Feature 15 | - Refactor 16 | - Tests 17 | - Documentation 18 | 19 | # Acceptance 20 | 21 | Please describe how to validate the feature or fix 22 | 23 | 1. go to X 24 | 2. test Y 25 | 3. assert Z 26 | 27 | If feature deals with theme / UI or internal elements used also in CARTO 3, please also add a note on how to do acceptance on that part. 28 | 29 | # Basic checklist 30 | 31 | - Good PR name 32 | - Shortcut link 33 | - Changelog entry 34 | - Just one issue per PR 35 | - GitHub labels 36 | - Proper status & reviewers 37 | - Tests 38 | - Documentation 39 | -------------------------------------------------------------------------------- /LICENSE.MD: -------------------------------------------------------------------------------- 1 | Copyright 2023 (CARTO) 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /packages/react-widgets/src/widgets/utils/WidgetWithAlert.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { NoDataAlert } from '@carto/react-ui'; 3 | 4 | export default function WidgetWithAlert({ 5 | noDataAlertProps = {}, 6 | warning, 7 | stableHeight, // if specified, "no-data" state will attempt to keep the same height as when rendered with data 8 | children 9 | }) { 10 | const [childrenRef, setChildenRef] = useState(); 11 | const [savedHeight, setSavedHeight] = useState(); 12 | const noData = warning || !children; 13 | 14 | if (stableHeight) { 15 | if (noData && childrenRef && savedHeight === undefined) { 16 | setSavedHeight(childrenRef?.clientHeight); 17 | } else if (!noData && savedHeight !== undefined) { 18 | setSavedHeight(undefined); 19 | } 20 | } 21 | 22 | return noData ? ( 23 | 27 | ) : ( 28 |
{children}
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/react-core/__tests__/utils/makeIntervalComplete.test.js: -------------------------------------------------------------------------------- 1 | import { makeIntervalComplete } from '../../src/utils/makeIntervalComplete'; 2 | 3 | describe('make interval complete', () => { 4 | test('first value is undefined', () => { 5 | const DATA = [[undefined, 1]]; 6 | expect(makeIntervalComplete(DATA)).toEqual([[Number.MIN_SAFE_INTEGER, 1]]); 7 | }); 8 | 9 | test('first value is null', () => { 10 | const DATA = [[null, 1]]; 11 | expect(makeIntervalComplete(DATA)).toEqual([[Number.MIN_SAFE_INTEGER, 1]]); 12 | }); 13 | 14 | test('last value is undefined', () => { 15 | const DATA = [[1, undefined]]; 16 | expect(makeIntervalComplete(DATA)).toEqual([[1, Number.MAX_SAFE_INTEGER]]); 17 | }); 18 | 19 | test('last value is null', () => { 20 | const DATA = [[1, null]]; 21 | expect(makeIntervalComplete(DATA)).toEqual([[1, Number.MAX_SAFE_INTEGER]]); 22 | }); 23 | 24 | test('both values are not undefined or null', () => { 25 | const DATA = [[1, 1]]; 26 | expect(makeIntervalComplete(DATA)).toEqual([[1, 1]]); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/Typography.d.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { TypographyTypeMap as MuiTypographyTypeMap } from '@mui/material/Typography'; 3 | import { OverridableComponent, OverrideProps } from '@mui/material/OverridableComponent'; 4 | 5 | export type TypographyTypeMap = 6 | MuiTypographyTypeMap< 7 | { 8 | /** 9 | * Font weight for Carto typography: 10 | * 11 | * * `'regular'` - 400 12 | * * `'medium'` - 500 13 | * * `'strong'` - 600 14 | */ 15 | weight?: CartoFontWeight; 16 | italic?: boolean; 17 | }, 18 | D 19 | >; 20 | 21 | export type CartoFontWeight = 'regular' | 'medium' | 'strong'; 22 | 23 | export type TypographyProps< 24 | D extends React.ElementType = TypographyTypeMap['defaultComponent'] 25 | > = OverrideProps, D>; 26 | 27 | // https://github.com/mui/material-ui/issues/19536#issuecomment-598856255 28 | declare const Typography: OverridableComponent; 29 | export default Typography; 30 | -------------------------------------------------------------------------------- /packages/react-ui/src/assets/icons/OpacityIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SvgIcon } from '@mui/material'; 3 | 4 | export default function OpacityIcon(props) { 5 | return ( 6 | 7 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/react-api/__tests__/hooks/mask-extension-util.test.js: -------------------------------------------------------------------------------- 1 | import { MASK_ID } from '@carto/react-core/'; 2 | import { MaskExtension } from '@deck.gl/extensions'; 3 | import { getMaskExtensionProps } from '../../src/hooks/maskExtensionUtil'; 4 | 5 | describe('mask-extension-util', () => { 6 | test('correct values without maskPolygon', () => { 7 | const myMaskPolygon = undefined; 8 | const { maskId, extensions } = getMaskExtensionProps(myMaskPolygon); 9 | 10 | expect(maskId).toEqual(false); 11 | expect(extensions[0]).toBeInstanceOf(MaskExtension); 12 | }); 13 | 14 | test('correct values width maskPolygon', () => { 15 | const myMaskPolygon = [ 16 | [-41.484375, 35.17380831799959], 17 | [0.3515625, 35.17380831799959], 18 | [0.3515625, 53.330872983017066], 19 | [-41.484375, 53.330872983017066], 20 | [-41.484375, 35.17380831799959] 21 | ]; 22 | const { maskId, extensions } = getMaskExtensionProps(myMaskPolygon); 23 | 24 | expect(maskId).toEqual(MASK_ID); 25 | expect(extensions[0]).toBeInstanceOf(MaskExtension); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/atoms/LabelWithIndicatorGuide.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # LabelWithIndicator 6 | 7 | Designers had the need to mark inputs as `optional` or `required` depending on the use case. That is, in some forms, we have the optional ones marked but in others the required ones. 8 | 9 | Mui only provides a `required` bool prop to mark an input as mandatory. When is true, add an asterisk at the end of the label and the validation logic. 10 | 11 | We removed the asterisk in the theme, and handle both, required and optional indicators, with this new component. So we can use the following: 12 | 13 | Required 14 | 15 | ``` 16 | } 19 | /> 20 | ``` 21 | 22 | Optional 23 | 24 | ``` 25 | } 28 | /> 29 | ``` 30 | 31 | Use ` ` from: `react-ui/src/components/atoms/Avatar` 32 | 33 | For external use: `import { LabelWithIndicator } from '@carto/react-ui';`. 34 | -------------------------------------------------------------------------------- /packages/react-core/src/utils/requestsUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Threshold to use GET requests, vs POST 3 | */ 4 | export const REQUEST_GET_MAX_URL_LENGTH = 2048; 5 | 6 | /** 7 | * Simple GET request 8 | */ 9 | export function getRequest(url, opts, headers = {}) { 10 | const { headers: extraHeaders, ...extraOpts } = opts; 11 | return new Request(url, { 12 | method: 'GET', 13 | headers: { 14 | Accept: 'application/json', 15 | ...headers, 16 | ...extraHeaders 17 | }, 18 | ...extraOpts 19 | }); 20 | } 21 | 22 | /** 23 | * Simple POST request 24 | */ 25 | export function postRequest(url, payload, opts, headers = {}) { 26 | const { headers: extraHeaders, ...extraOpts } = opts; 27 | return new Request(url, { 28 | method: 'POST', 29 | headers: { 30 | Accept: 'application/json', 31 | 'Content-Type': 'application/json', 32 | ...headers, 33 | ...extraHeaders 34 | }, 35 | body: JSON.stringify(payload), 36 | ...extraOpts 37 | }); 38 | } 39 | 40 | /** 41 | * Simple encode parameter 42 | */ 43 | export function encodeParameter(name, value) { 44 | return `${name}=${encodeURIComponent(value)}`; 45 | } 46 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/Menu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Menu as MuiMenu, styled } from '@mui/material'; 4 | 5 | const StyledMenu = styled(MuiMenu, { 6 | shouldForwardProp: (prop) => !['extended', 'width', 'height'].includes(prop) 7 | })(({ extended, width, height, theme }) => ({ 8 | ...(extended && { 9 | '.MuiMenuItem-root': { 10 | minHeight: theme.spacing(6) 11 | } 12 | }), 13 | ...(width && { 14 | '.MuiList-root': { 15 | width: width, 16 | minWidth: width 17 | } 18 | }), 19 | ...(height && { 20 | '.MuiMenu-paper': { 21 | overflow: 'hidden' 22 | }, 23 | '.MuiList-root': { 24 | maxHeight: height 25 | } 26 | }) 27 | })); 28 | 29 | const Menu = ({ extended, width, height, children, ...otherProps }) => { 30 | return ( 31 | 32 | {children} 33 | 34 | ); 35 | }; 36 | 37 | Menu.propTypes = { 38 | extended: PropTypes.bool, 39 | width: PropTypes.string, 40 | height: PropTypes.string 41 | }; 42 | 43 | export default Menu; 44 | -------------------------------------------------------------------------------- /packages/react-widgets/src/hooks/useWidgetSource.js: -------------------------------------------------------------------------------- 1 | import { _getApplicableFilters as getApplicableFilters } from '@carto/react-core/'; 2 | import { selectSourceById } from '@carto/react-redux/'; 3 | import { useMemo } from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import useCustomCompareMemo from './useCustomCompareMemo'; 6 | import { dequal as deepEqual } from 'dequal'; 7 | 8 | /** 9 | * Hook to obtain widget's source excluding the filters like in useSourceFilters 10 | * @param {object} props 11 | * @param {string} props.dataSource - ID of the source to get the filters from. 12 | * @param {string} props.id - ID of the widget that apply the filter you want to exclude. 13 | */ 14 | export default function useWidgetSource({ dataSource, id }) { 15 | const rawSource = useSelector((state) => selectSourceById(state, dataSource)); 16 | 17 | const applicableFilters = useMemo( 18 | () => getApplicableFilters(rawSource?.filters, id), 19 | [rawSource?.filters, id] 20 | ); 21 | 22 | return useCustomCompareMemo( 23 | rawSource && { 24 | ...rawSource, 25 | filters: applicableFilters 26 | }, 27 | deepEqual 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/foundations/Breakpoints.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BREAKPOINTS } from '../../../src/theme/themeConstants'; 3 | import Typography from '../../../src/components/atoms/Typography'; 4 | import { FilledContainer } from '../../utils/storyStyles'; 5 | 6 | const options = { 7 | title: 'Foundations/Breakpoints', 8 | argTypes: { 9 | breakpoint: { 10 | defaultValue: '100%', 11 | control: { 12 | type: 'select', 13 | options: { ...BREAKPOINTS } 14 | } 15 | } 16 | }, 17 | parameters: { 18 | design: { 19 | type: 'figma', 20 | url: 'https://www.figma.com/file/lVrTKiHj5zFUmCjjHF6Rc4/CARTO-Foundations?node-id=10472%3A3871' 21 | }, 22 | status: { 23 | type: 'validated' 24 | } 25 | } 26 | }; 27 | export default options; 28 | 29 | const BreakpointBox = ({ breakpoint }) => { 30 | return ( 31 | 36 | {breakpoint} 37 | 38 | ); 39 | }; 40 | 41 | export const Breakpoints = BreakpointBox.bind({}); 42 | -------------------------------------------------------------------------------- /packages/react-widgets/src/hooks/useSourceFilters.js: -------------------------------------------------------------------------------- 1 | import { _getApplicableFilters as getApplicableFilters } from '@carto/react-core/'; 2 | import { selectSourceById } from '@carto/react-redux/'; 3 | import { useMemo } from 'react'; 4 | import { useSelector } from 'react-redux'; 5 | import useCustomCompareMemo from './useCustomCompareMemo'; 6 | import { dequal as deepEqual } from 'dequal'; 7 | 8 | /** 9 | * Hook to obtain source's filters excluding the one from a certain widget 10 | * @param {object} props 11 | * @param {string} props.dataSource - ID of the source to get the filters from. 12 | * @param {string} props.id - ID of the widget that apply the filter you want to exclude. 13 | */ 14 | export default function useSourceFilters({ dataSource, id }) { 15 | const { filters, filtersLogicalOperator } = useSelector( 16 | (state) => selectSourceById(state, dataSource) || {} 17 | ); 18 | 19 | const applicableFilters = useMemo( 20 | () => getApplicableFilters(filters, id), 21 | [filters, id] 22 | ); 23 | 24 | return useCustomCompareMemo( 25 | { 26 | filters: applicableFilters, 27 | filtersLogicalOperator 28 | }, 29 | deepEqual 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/LoadingTemplateWithSwitch.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Label, ThinContainer } from '../../utils/storyStyles'; 3 | 4 | /** 5 | * Create template for testing loading/loaded state layoyut. 6 | * 7 | * Target is to have same size of widget and position of components in loaded/loading(skeleton). 8 | * 9 | * @param {*} Component component to be tested 10 | * @returns Template 11 | */ 12 | const LoadingTemplateWithSwitch = (Component) => (args) => { 13 | // eslint-disable-next-line react-hooks/rules-of-hooks 14 | const [isLoading, setIsLoading] = useState(args.isLoading); 15 | return ( 16 | <> 17 | 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default LoadingTemplateWithSwitch; 34 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/CategoryWidgetUI/CategorySkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, styled } from '@mui/material'; 3 | import { Skeleton } from '@mui/material'; 4 | 5 | import { CategoriesWrapper, CategoryItemGroup } from './CategoryWidgetUI.styled'; 6 | 7 | const SkeletonProgressbar = styled(Skeleton)(({ theme }) => ({ 8 | marginTop: theme.spacing(1.25), 9 | marginBottom: theme.spacing(1.75) 10 | })); 11 | 12 | const CategorySkeleton = () => { 13 | const isOdd = (num) => num % 2 === 1; 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 20 | {[...Array(6)].map((_, i) => ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ))} 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default CategorySkeleton; 36 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/foundations/PaletteGuide.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # Palette 6 | 7 | ### New colors 8 | 9 | - `default` variant: new set of neutral colors. Use it instead of `grey palette`. 10 | - `brand palette` with custom CARTO colors for branding. 11 | - We also have a set of `shade` colors (with transparency): 12 | - `black` 13 | - `white` 14 | 15 | ### Deleted colors 16 | 17 | Some keys have been removed from [color palette](https://github.com/CartoDB/carto-react/blob/master/packages/react-ui/src/theme/sections/palette.js) due they are unused: 18 | 19 | - `activatedOpacity` 20 | - `hoverOpacity` 21 | - `disabledOpacity` 22 | - `selectedOpacity` 23 | - `focusOpacity` 24 | - `other`, all removed but `divider`, which is moved to first level 25 | 26 | ### Replaced colors 27 | 28 | Some others have been moved or replaced because they aren't native MUI keys and are so specific to some components, these are: 29 | 30 | - `charts`: replaced by `theme.palette.black[%]` 31 | - `primary.relatedLight`: replaced by `primary.background` 32 | - `secondary.relatedLight`: replaced by `secondary.background`. 33 | -------------------------------------------------------------------------------- /packages/react-redux/src/slices/oauthSlice.d.ts: -------------------------------------------------------------------------------- 1 | import { OauthApp } from '@carto/react-auth/'; 2 | import { InitialOauthState, OauthState } from '../types'; 3 | import { AnyAction, Reducer } from 'redux'; 4 | 5 | type OauthParams = { 6 | accessToken: string, 7 | expirationDate: string, 8 | userInfoUrl: string 9 | } 10 | 11 | type OauthError = { 12 | error: string, 13 | errorDescription: string 14 | } 15 | 16 | declare enum OauthActions { 17 | SET_OAUTH_APP = 'carto/setOAuthApp', 18 | SET_TOKEN_AND_USER_INFO = 'carto/setTokenAndUserInfo', 19 | LOGOUT = 'oauth/logout' 20 | } 21 | 22 | export function createOauthCartoSlice(initialState: InitialOauthState): Reducer; 23 | 24 | export function setOAuthApp(arg: OauthApp): { 25 | type: OauthActions.SET_OAUTH_APP, 26 | payload: OauthApp 27 | }; 28 | 29 | export function setTokenAndUserInfo(payload: OauthParams | OauthError): { 30 | type: OauthActions.SET_TOKEN_AND_USER_INFO, 31 | payload: OauthParams | OauthError 32 | }; 33 | 34 | export const setTokenAndUserInfoAsync: Function; 35 | 36 | export function logout(): { 37 | type: OauthActions.LOGOUT, 38 | payload: {} 39 | }; 40 | 41 | export const selectOAuthCredentials: Function; -------------------------------------------------------------------------------- /packages/react-ui/src/components/organisms/AppBar/BurgerMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Divider, Hidden, IconButton } from '@mui/material'; 3 | import { alpha, styled } from '@mui/material/styles'; 4 | import { MenuOutlined } from '@mui/icons-material'; 5 | 6 | import { APPBAR_SIZE } from '../../../theme/themeConstants'; 7 | 8 | const Menu = styled('div')(({ theme }) => ({ 9 | display: 'flex', 10 | alignItems: 'center', 11 | height: APPBAR_SIZE, 12 | marginRight: theme.spacing(1.5) 13 | })); 14 | 15 | const MenuButton = styled(IconButton)(({ theme }) => ({ 16 | marginRight: theme.spacing(1), 17 | 18 | '&.MuiButtonBase-root svg path': { 19 | fill: theme.palette.brand.appBarContrastText 20 | } 21 | })); 22 | 23 | const MenuDivider = styled(Divider)(({ theme }) => ({ 24 | borderColor: alpha(theme.palette.brand.appBarContrastText, 0.12) 25 | })); 26 | 27 | export default function BurgerMenu({ onClickMenu }) { 28 | return ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/NoDataAlert.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NoDataAlert from '../../../src/widgets/NoDataAlert'; 3 | import { buildReactPropsAsString } from '../../utils/utils'; 4 | 5 | const options = { 6 | title: 'Widgets/NoDataAlert', 7 | component: NoDataAlert, 8 | argTypes: { 9 | title: { 10 | table: { 11 | type: { 12 | summary: 'string' 13 | } 14 | }, 15 | control: { type: 'text' } 16 | }, 17 | body: { 18 | table: { 19 | type: { 20 | summary: 'string' 21 | } 22 | }, 23 | control: { type: 'text' } 24 | } 25 | } 26 | }; 27 | 28 | export default options; 29 | 30 | const Template = (args) => ; 31 | 32 | export const Empty = Template.bind({}); 33 | Empty.args = {}; 34 | Empty.parameters = buildReactPropsAsString({}, 'NoDataAlert'); 35 | 36 | export const CustomTexts = Template.bind({}); 37 | const CustomTextsProps = { 38 | title: 'Example', 39 | body: "Hey, I've modified the NoDataAlert component" 40 | }; 41 | CustomTexts.args = CustomTextsProps; 42 | CustomTexts.parameters = buildReactPropsAsString(CustomTextsProps, 'NoDataAlert'); 43 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/RangeWidgetUI/RangeSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, styled } from '@mui/material'; 3 | import { Skeleton } from '@mui/material'; 4 | import { SkeletonSolid } from '../SkeletonWidgets'; 5 | 6 | const Root = styled(Grid)(({ theme }) => ({ 7 | position: 'relative', 8 | alignItems: 'center', 9 | height: theme.spacing(4) 10 | })); 11 | 12 | const DotsContainer = styled(Grid)(({ theme }) => ({ 13 | position: 'absolute', 14 | zIndex: 1, 15 | padding: theme.spacing(0, 3) 16 | })); 17 | 18 | const RangeSkeleton = () => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | ); 36 | }; 37 | 38 | export default RangeSkeleton; 39 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/foundations/SpacingGuide.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # Spacing 6 | 7 | Design is restringed to a few specific values for spacing, which are: 8 | 9 | `0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 7, 8, 9, 12, 15`. 10 | 11 | ## theme.spacing 12 | 13 | We have a new custom spacing constant in carto-theme, `spacingValue`, which you should use instead of the common `theme.spacing()` function in cases where you need to do value calculations, because since Mui v5, theme.spacing is no longer a number, but a string in this format: `number + px`. 14 | 15 | Note that if you're using `calc()` in your styles, you can keep using `theme.spacing()` as usual. 16 | 17 | `theme.spacingValue * 2` 18 | 19 | Needed changes: 20 | 21 | 1. Change `${theme.spacing(xx)}px` by `${theme.spacing(xx)}`. It means, without the `px` ending, since in Mui v5 it is appended to the end of the string by default. 22 | 23 | Tip: An easy search to catch up this, would be `)}px` 24 | 25 | 2. Change `-theme.spacing(xx)` by `theme.spacing(-xx)`. It means, move the negative symbol inside the function. 26 | 27 | Tip: An easy search to catch up this, would be `-theme.spacing(` 28 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/utils/testUtils.js: -------------------------------------------------------------------------------- 1 | // https://testing-library.com/docs/react-testing-library/setup/ 2 | 3 | import React from 'react'; 4 | import { render } from '@testing-library/react'; 5 | 6 | import { ThemeProvider } from '@mui/material'; 7 | import { IntlProvider } from 'react-intl'; 8 | 9 | import { createTheme, responsiveFontSizes } from '@mui/material'; 10 | 11 | import { cartoThemeOptions } from '../../../src/theme/carto-theme'; 12 | 13 | const theme = getTheme(); // for now we don't need real theme for tests 14 | 15 | function getTheme() { 16 | let theme = createTheme(cartoThemeOptions); 17 | theme = responsiveFontSizes(theme, { 18 | breakpoints: ['sm'], 19 | disableAlign: false, 20 | factor: 2 21 | }); 22 | return theme; 23 | } 24 | 25 | const AllTheProviders = ({ children }) => { 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | const customRender = (ui, options) => 34 | render(ui, { wrapper: AllTheProviders, ...options }); 35 | 36 | // re-export everything 37 | export * from '@testing-library/react'; 38 | 39 | // override render method 40 | export { customRender as render }; 41 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/Typography.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Typography as MuiTypography } from '@mui/material'; 4 | 5 | const FontWeight = { 6 | regular: 400, 7 | medium: 500, 8 | strong: 600 9 | }; 10 | 11 | const Typography = forwardRef( 12 | ({ italic, weight, style, children, ...otherProps }, ref) => { 13 | // forwardRef needed to be able to hold a reference, in this way it can be a child for some Mui components, like Tooltip 14 | // https://mui.com/material-ui/guides/composition/#caveat-with-refs 15 | const fontConfiguration = { 16 | fontWeight: FontWeight[weight], 17 | fontStyle: italic && 'italic' 18 | }; 19 | 20 | return ( 21 | 29 | {children} 30 | 31 | ); 32 | } 33 | ); 34 | 35 | Typography.propTypes = { 36 | weight: PropTypes.oneOf(Object.keys(FontWeight)), 37 | italic: PropTypes.bool, 38 | style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]) 39 | }; 40 | 41 | export default Typography; 42 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/testUtils.js: -------------------------------------------------------------------------------- 1 | import * as echarts from 'echarts'; 2 | 3 | export function currencyFormatter(value) { 4 | return { 5 | prefix: '$', 6 | value: Intl.NumberFormat('en-US', { 7 | maximumFractionDigits: 2, 8 | minimumFractionDigits: 2, 9 | notation: 'compact', 10 | compactDisplay: 'short' 11 | }).format(isNaN(value) ? 0 : value) 12 | }; 13 | } 14 | 15 | export const mockEcharts = { 16 | init() { 17 | jest.spyOn(echarts, 'getInstanceByDom').mockImplementation(() => ({ 18 | dispatchAction: jest.fn(), 19 | hideLoading: jest.fn(), 20 | getOption: jest.fn(() => ({ 21 | series: [ 22 | { 23 | data: [ 24 | { 25 | disabled: true, 26 | itemStyle: {} 27 | } 28 | ] 29 | } 30 | ] 31 | })), 32 | setOption: jest.fn(() => ({ 33 | disabled: true, 34 | itemStyle: {} 35 | })), 36 | showLoading: jest.fn(), 37 | on: jest.fn(), 38 | off: jest.fn(), 39 | getZr: jest.fn(), 40 | resize: jest.fn(), 41 | getDom: jest.fn() 42 | })); 43 | }, 44 | destroy() { 45 | jest.restoreAllMocks(); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/UploadField/FilesAction.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button, CircularProgress, IconButton, InputAdornment } from '@mui/material'; 3 | import { Cancel } from '@mui/icons-material'; 4 | 5 | // For Browser or Delete actions 6 | function FilesAction({ 7 | buttonText, 8 | hasFiles, 9 | size, 10 | error, 11 | disabled, 12 | handleReset, 13 | handleOpen, 14 | inProgress 15 | }) { 16 | return ( 17 | 18 | {inProgress ? ( 19 | 20 | 21 | 22 | ) : !hasFiles ? ( 23 | 32 | ) : ( 33 | 39 | 40 | 41 | )} 42 | 43 | ); 44 | } 45 | 46 | export default FilesAction; 47 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/CategoryModel.js: -------------------------------------------------------------------------------- 1 | import { _executeModel } from '@carto/react-api/'; 2 | import { Methods, executeTask } from '@carto/react-workers'; 3 | import { normalizeObjectKeys, wrapModelCall } from './utils'; 4 | 5 | export function getCategories(props) { 6 | return wrapModelCall(props, fromLocal, fromRemote); 7 | } 8 | 9 | // From local 10 | function fromLocal(props) { 11 | const { source, column, operationColumn, operation, joinOperation } = props; 12 | 13 | return executeTask(source.id, Methods.FEATURES_CATEGORY, { 14 | filters: source.filters, 15 | filtersLogicalOperator: source.filtersLogicalOperator, 16 | operation, 17 | joinOperation, 18 | column, 19 | operationColumn: operationColumn || column 20 | }); 21 | } 22 | 23 | // From remote 24 | function fromRemote(props) { 25 | const { source, spatialFilter, abortController, ...params } = props; 26 | const { column, operation, operationColumn } = params; 27 | 28 | return _executeModel({ 29 | model: 'category', 30 | source, 31 | spatialFilter, 32 | params: { 33 | column, 34 | operation, 35 | operationColumn: operationColumn || column 36 | }, 37 | opts: { abortController } 38 | }).then((res) => normalizeObjectKeys(res.rows)); 39 | } 40 | -------------------------------------------------------------------------------- /packages/react-basemaps/__tests__/basemaps.test.js: -------------------------------------------------------------------------------- 1 | import { BASEMAPS } from '../src/basemaps/basemaps'; 2 | 3 | describe('basemaps', () => { 4 | test('should export allowed basemaps', () => { 5 | expect(Object.keys(BASEMAPS)).toEqual([ 6 | 'positron', 7 | 'voyager', 8 | 'dark-matter', 9 | 'roadmap', 10 | 'satellite', 11 | 'hybrid', 12 | 'custom' 13 | ]); 14 | }); 15 | 16 | test('should has a valid type', () => { 17 | for (const value of Object.values(BASEMAPS)) { 18 | expect(value.type === 'mapbox' || value.type === 'gmaps').toBe(true); 19 | } 20 | }); 21 | 22 | describe('options', () => { 23 | for (const [key, value] of Object.entries(BASEMAPS)) { 24 | if ('mapStyle' in value.options) { 25 | test('carto basemaps should include a mapbox style json url', () => { 26 | expect(value.options.mapStyle).toEqual( 27 | `https://basemaps.cartocdn.com/gl/${key}-gl-style/style.json` 28 | ); 29 | }); 30 | } 31 | 32 | if ('mapTypeId' in value.options) { 33 | test('google basemaps should include a mapTypeId that matches own object key', () => { 34 | expect(value.options.mapTypeId).toEqual(key); 35 | }); 36 | } 37 | } 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /packages/react-api/src/api/tilejson.js: -------------------------------------------------------------------------------- 1 | import { checkCredentials, CLIENT_ID } from './common'; 2 | import { vectorTilesetSource } from '@deck.gl/carto'; 3 | import { _assert as assert } from '@carto/react-core'; 4 | import { MAP_TYPES, API_VERSIONS } from '../types'; 5 | 6 | /** 7 | * Get the TileJson for static tilesets 8 | * 9 | * @param { object } props 10 | * @param { object } props.source - A static tileset C4R source 11 | */ 12 | export async function getTileJson(props) { 13 | const { source } = props; 14 | 15 | assert(source, 'getTileJson: missing source'); 16 | assert(source.connection, 'getTileJson: missing connection'); 17 | assert(source.data, 'getTileJson: missing data'); 18 | assert( 19 | source.type === MAP_TYPES.TILESET, 20 | 'getTileJson: source must be a static tileset' 21 | ); 22 | checkCredentials(source.credentials); 23 | assert( 24 | source.credentials.apiVersion === API_VERSIONS.V3, 25 | 'TileJson is a feature only available in CARTO 3.' 26 | ); 27 | 28 | const data = await vectorTilesetSource({ 29 | connectionName: source.connection, 30 | apiBaseUrl: source.credentials.apiBaseUrl, 31 | accessToken: source.credentials.accessToken, 32 | clientId: CLIENT_ID, 33 | tableName: source.data 34 | }); 35 | 36 | return data; 37 | } 38 | -------------------------------------------------------------------------------- /packages/react-core/__tests__/operations/scatterPlot.test.js: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from '@carto/react-core/'; 2 | import { scatterPlot } from '../../src/operations/scatterPlot'; 3 | 4 | describe('scatterPlot', () => { 5 | test('should filter invalid values', () => { 6 | const data = [ 7 | { x: 0 }, // Missing y 8 | { y: 1 }, // Missing x 9 | { x: null, y: 1 }, // null x 10 | { x: 1, y: null }, // null y 11 | { x: 0, y: 0 }, // zero for both 12 | { x: 1, y: 2 }, // valid 13 | {}, // no values for both 14 | { x: 2, y: 3 } // valid 15 | ]; 16 | 17 | expect(scatterPlot({ data, xAxisColumns: ['x'], yAxisColumns: ['y'] })).toEqual([ 18 | [0, 0], 19 | [1, 2], 20 | [2, 3] 21 | ]); 22 | }); 23 | 24 | test('using multiple columns', () => { 25 | const data = [ 26 | { x: 0, y: 0 }, 27 | { x: 1, y: 2 }, 28 | { x: 2, y: 3 } 29 | ]; 30 | 31 | expect( 32 | scatterPlot({ 33 | data, 34 | xAxisColumns: ['x', 'y'], 35 | xAxisJoinOperation: AggregationTypes.SUM, 36 | yAxisColumns: ['x', 'y'], 37 | yAxisJoinOperation: AggregationTypes.SUM 38 | }) 39 | ).toEqual([ 40 | [0, 0], 41 | [3, 3], 42 | [5, 5] 43 | ]); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/atoms/PasswordField.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useState } from 'react'; 2 | import { IconButton, InputAdornment, TextField } from '@mui/material'; 3 | import { VisibilityOffOutlined, VisibilityOutlined } from '@mui/icons-material'; 4 | 5 | const PasswordField = forwardRef(({ InputProps, size = 'small', ...otherProps }, ref) => { 6 | // forwardRef needed to be able to hold a reference, in this way it can be a child for some Mui components, like Tooltip 7 | // https://mui.com/material-ui/guides/composition/#caveat-with-refs 8 | const [showPassword, setShowPassword] = useState(false); 9 | const handleClickShowPassword = () => setShowPassword(!showPassword); 10 | 11 | return ( 12 | 21 | 22 | {showPassword ? : } 23 | 24 | 25 | ) 26 | }} 27 | /> 28 | ); 29 | }); 30 | 31 | export default PasswordField; 32 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/Autocomplete.d.ts: -------------------------------------------------------------------------------- 1 | import { ChipTypeMap } from '@mui/material'; 2 | import { AutocompleteProps as MuiAutocompleteProps } from '@mui/material/Autocomplete'; 3 | 4 | // Boilerplate to avoid Typescript error with generic types: we must repeat all original typings in declaration, so variants like multiple/freesolo, etc. have proper typings 5 | export type AutocompleteProps< 6 | Value, 7 | Multiple extends boolean | undefined, 8 | DisableClearable extends boolean | undefined, 9 | FreeSolo extends boolean | undefined, 10 | ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'] 11 | > = MuiAutocompleteProps & { 12 | creatable?: boolean; 13 | newItemLabel?: string | ((value: string) => React.ReactNode | string); 14 | newItemIcon?: React.ReactNode; 15 | }; 16 | 17 | declare const Autocomplete: < 18 | Value, 19 | Multiple extends boolean | undefined, 20 | DisableClearable extends boolean | undefined, 21 | FreeSolo extends boolean | undefined, 22 | ChipComponent extends React.ElementType = ChipTypeMap['defaultComponent'] 23 | >( 24 | props: AutocompleteProps 25 | ) => JSX.Element; 26 | export default Autocomplete; 27 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/NoDataAlert.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Alert, AlertTitle, Box } from '@mui/material'; 3 | import Typography from '../components/atoms/Typography'; 4 | 5 | function AlertBody({ color = undefined, children }) { 6 | return children ? ( 7 | 8 | 14 | {children} 15 | 16 | 17 | ) : ( 18 | 19 | ); 20 | } 21 | 22 | function NoDataAlert({ 23 | title = 'No data available', 24 | body = 'There are no results for the combination of filters applied to your data. Try tweaking your filters, or zoom and pan the map to adjust the Map View.', 25 | severity = undefined, 26 | ...otherProps 27 | }) { 28 | return severity ? ( 29 | 30 | {title && {title}} 31 | {body} 32 | 33 | ) : ( 34 | 35 | {title && {title}} 36 | {body} 37 | 38 | ); 39 | } 40 | 41 | export default NoDataAlert; 42 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/HistogramModel.js: -------------------------------------------------------------------------------- 1 | import { _executeModel } from '@carto/react-api'; 2 | import { Methods, executeTask } from '@carto/react-workers'; 3 | import { normalizeObjectKeys, wrapModelCall } from './utils'; 4 | 5 | export function getHistogram(props) { 6 | return wrapModelCall(props, fromLocal, fromRemote); 7 | } 8 | 9 | // From local 10 | function fromLocal(props) { 11 | const { source, column, operation, ticks } = props; 12 | 13 | return executeTask(source.id, Methods.FEATURES_HISTOGRAM, { 14 | filters: source.filters, 15 | filtersLogicalOperator: source.filtersLogicalOperator, 16 | operation, 17 | column, 18 | ticks 19 | }); 20 | } 21 | 22 | // From remote 23 | async function fromRemote(props) { 24 | const { source, spatialFilter, abortController, ...params } = props; 25 | const { column, operation, ticks } = params; 26 | 27 | const data = await _executeModel({ 28 | model: 'histogram', 29 | source, 30 | spatialFilter, 31 | params: { column, operation, ticks }, 32 | opts: { abortController } 33 | }).then((res) => normalizeObjectKeys(res.rows)); 34 | 35 | if (data.length) { 36 | const result = Array(ticks.length + 1).fill(0); 37 | data.forEach(({ tick, value }) => (result[tick] = value)); 38 | return result; 39 | } 40 | 41 | return []; 42 | } 43 | -------------------------------------------------------------------------------- /packages/react-api/src/hooks/useFeaturesCommons.js: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import { setFeaturesReady } from '@carto/react-redux'; 3 | import { useState, useRef, useEffect, useCallback } from 'react'; 4 | 5 | export default function useFeaturesCommons({ source }) { 6 | const dispatch = useDispatch(); 7 | 8 | const [isDataLoaded, setDataLoaded] = useState(false); 9 | const debounceIdRef = useRef(null); 10 | 11 | useEffect(() => { 12 | if (!source) { 13 | setDataLoaded(false); 14 | } 15 | }, [source]); 16 | 17 | const clearDebounce = useCallback(() => { 18 | if (debounceIdRef.current) { 19 | clearTimeout(debounceIdRef.current); 20 | } 21 | debounceIdRef.current = null; 22 | }, []); 23 | 24 | const stopAnyCompute = useCallback(() => { 25 | clearDebounce(); 26 | setDataLoaded(false); 27 | }, [clearDebounce, setDataLoaded]); 28 | 29 | const sourceId = source?.id; 30 | 31 | const setSourceFeaturesReady = useCallback( 32 | (ready) => { 33 | if (sourceId) { 34 | dispatch(setFeaturesReady({ sourceId, ready })); 35 | } 36 | }, 37 | [dispatch, sourceId] 38 | ); 39 | 40 | return [ 41 | debounceIdRef, 42 | isDataLoaded, 43 | setDataLoaded, 44 | clearDebounce, 45 | stopAnyCompute, 46 | setSourceFeaturesReady 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/groupBy.js: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from './constants/AggregationTypes'; 2 | import { aggregationFunctions, aggregate } from './aggregation'; 3 | 4 | export function groupValuesByColumn({ 5 | data, 6 | valuesColumns, 7 | joinOperation, 8 | keysColumn, 9 | operation 10 | }) { 11 | if (Array.isArray(data) && data.length === 0) { 12 | return null; 13 | } 14 | const groups = data.reduce((accumulator, item) => { 15 | const group = item[keysColumn]; 16 | 17 | const values = accumulator.get(group) || [] 18 | accumulator.set(group, values) 19 | 20 | const aggregatedValue = aggregate(item, valuesColumns, joinOperation); 21 | 22 | const isValid = 23 | (operation === AggregationTypes.COUNT ? true : aggregatedValue !== null) && 24 | aggregatedValue !== undefined; 25 | 26 | if (isValid) { 27 | values.push(aggregatedValue) 28 | accumulator.set(group, values); 29 | } 30 | 31 | return accumulator; 32 | }, new Map()); // We use a map to be able to maintain the type in the key value 33 | 34 | 35 | const targetOperation = aggregationFunctions[operation]; 36 | 37 | if (targetOperation) { 38 | return Array.from(groups).map(([name, value]) => ({ 39 | name, 40 | value: targetOperation(value) 41 | })); 42 | } 43 | 44 | return []; 45 | } 46 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/FormulaModel.js: -------------------------------------------------------------------------------- 1 | import { _executeModel } from '@carto/react-api'; 2 | import { Methods, executeTask } from '@carto/react-workers'; 3 | import { normalizeObjectKeys, wrapModelCall } from './utils'; 4 | import { AggregationTypes } from '@carto/react-core'; 5 | 6 | export function getFormula(props) { 7 | return wrapModelCall(props, fromLocal, fromRemote); 8 | } 9 | 10 | // From local 11 | function fromLocal(props) { 12 | const { source, operation, column, joinOperation } = props; 13 | 14 | if (operation === AggregationTypes.CUSTOM) { 15 | throw new Error('Custom aggregation not supported for local widget calculation'); 16 | } 17 | return executeTask(source.id, Methods.FEATURES_FORMULA, { 18 | filters: source.filters, 19 | filtersLogicalOperator: source.filtersLogicalOperator, 20 | operation, 21 | joinOperation, 22 | column 23 | }); 24 | } 25 | 26 | // From remote 27 | function fromRemote(props) { 28 | const { source, spatialFilter, abortController, operationExp, ...params } = props; 29 | const { column, operation } = params; 30 | 31 | return _executeModel({ 32 | model: 'formula', 33 | source, 34 | spatialFilter, 35 | params: { column: column ?? '*', operation, operationExp }, 36 | opts: { abortController } 37 | }).then((res) => normalizeObjectKeys(res.rows[0])); 38 | } 39 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/Avatar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Avatar as MuiAvatar } from '@mui/material'; 4 | import { styled } from '@mui/material/styles'; 5 | import { ICON_SIZE_SMALL } from '../../theme/themeConstants'; 6 | 7 | const sizes = { 8 | large: 5, 9 | medium: 4, 10 | small: 3, 11 | xsmall: 2.25 12 | }; 13 | 14 | const AvatarContainer = styled(MuiAvatar, { 15 | shouldForwardProp: (prop) => prop !== 'size' 16 | })(({ size, theme }) => ({ 17 | width: theme.spacing(sizes[size]), 18 | height: theme.spacing(sizes[size]), 19 | ...theme.typography.subtitle1, 20 | 21 | ...(size === 'large' && { 22 | ...theme.typography.h6 23 | }), 24 | ...(size === 'small' && { 25 | ...theme.typography.caption, 26 | fontWeight: 500 27 | }), 28 | ...(size === 'xsmall' && { 29 | ...theme.typography.caption, 30 | fontWeight: 500, 31 | 32 | svg: { 33 | width: ICON_SIZE_SMALL, 34 | height: ICON_SIZE_SMALL 35 | } 36 | }) 37 | })); 38 | 39 | const Avatar = ({ size = 'medium', children, ...otherProps }) => { 40 | return ( 41 | 42 | {children} 43 | 44 | ); 45 | }; 46 | 47 | Avatar.propTypes = { 48 | size: PropTypes.oneOf(Object.keys(sizes)) 49 | }; 50 | 51 | export default Avatar; 52 | -------------------------------------------------------------------------------- /packages/react-core/src/operations/histogram.js: -------------------------------------------------------------------------------- 1 | import { aggregate, aggregationFunctions } from './aggregation'; 2 | 3 | export function histogram({ data, valuesColumns, joinOperation, ticks, operation }) { 4 | if (Array.isArray(data) && data.length === 0) { 5 | return []; 6 | } 7 | 8 | const binsContainer = [Number.MIN_SAFE_INTEGER, ...ticks].map((tick, index, arr) => ({ 9 | bin: index, 10 | start: tick, 11 | end: index === arr.length - 1 ? Number.MAX_SAFE_INTEGER : arr[index + 1], 12 | values: [] 13 | })); 14 | 15 | data.forEach((feature) => { 16 | const featureValue = aggregate(feature, valuesColumns, joinOperation); 17 | 18 | const isValid = featureValue !== null && featureValue !== undefined; 19 | 20 | if (!isValid) { 21 | return; 22 | } 23 | 24 | const binContainer = binsContainer.find( 25 | (bin) => bin.start <= featureValue && bin.end > featureValue 26 | ); 27 | 28 | if (!binContainer) { 29 | return; 30 | } 31 | 32 | binContainer.values.push(featureValue); 33 | }); 34 | 35 | const targetOperation = aggregationFunctions[operation]; 36 | 37 | if (targetOperation) { 38 | const transformedBins = binsContainer.map((binContainer) => binContainer.values); 39 | return transformedBins.map((values) => (values.length ? targetOperation(values) : 0)); 40 | } 41 | 42 | return []; 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/legend/LegendLayerTitle.js: -------------------------------------------------------------------------------- 1 | import React, { useLayoutEffect, useRef, useState } from 'react'; 2 | import { Tooltip } from '@mui/material'; 3 | import Typography from '../../components/atoms/Typography'; 4 | 5 | /** Renders the legend layer title with an optional tooltip if the title is detected to be too long. 6 | * @param {object} props 7 | * @param {string} props.title 8 | * @param {boolean} props.visible 9 | * @param {object} props.typographyProps 10 | * @returns {React.ReactNode} 11 | */ 12 | export default function LegendLayerTitle({ title, visible, typographyProps }) { 13 | const ref = useRef(null); 14 | const [isOverflow, setIsOverflow] = useState(false); 15 | 16 | useLayoutEffect(() => { 17 | if (visible && ref.current) { 18 | const { offsetWidth, scrollWidth } = ref.current; 19 | setIsOverflow(offsetWidth < scrollWidth); 20 | } 21 | }, [title, visible]); 22 | 23 | const element = ( 24 | 34 | {title} 35 | 36 | ); 37 | 38 | if (!isOverflow) { 39 | return element; 40 | } 41 | 42 | return {element}; 43 | } 44 | -------------------------------------------------------------------------------- /packages/react-workers/src/workers/features.worker.js: -------------------------------------------------------------------------------- 1 | import { Methods } from '../workerMethods'; 2 | import { 3 | getTileFeatures, 4 | getFormula, 5 | getHistogram, 6 | getCategories, 7 | getScatterPlot, 8 | getTimeSeries, 9 | getRawFeatures, 10 | getRange, 11 | loadTiles, 12 | loadGeoJSONFeatures, 13 | getGeojsonFeatures 14 | } from './methods'; 15 | 16 | export const methodMap = { 17 | [Methods.TILE_FEATURES]: getTileFeatures, 18 | [Methods.FEATURES_FORMULA]: getFormula, 19 | [Methods.FEATURES_HISTOGRAM]: getHistogram, 20 | [Methods.FEATURES_CATEGORY]: getCategories, 21 | [Methods.FEATURES_SCATTERPLOT]: getScatterPlot, 22 | [Methods.FEATURES_TIME_SERIES]: getTimeSeries, 23 | [Methods.FEATURES_RAW]: getRawFeatures, 24 | [Methods.FEATURES_RANGE]: getRange, 25 | [Methods.LOAD_TILES]: loadTiles, 26 | [Methods.LOAD_GEOJSON_FEATURES]: loadGeoJSONFeatures, 27 | [Methods.GEOJSON_FEATURES]: getGeojsonFeatures 28 | }; 29 | 30 | onmessage = ({ data: { method, ...params } }) => { 31 | try { 32 | const methodFn = methodMap[method]; 33 | if (!methodFn) { 34 | throw new Error(`Invalid react-workers method: ${methodFn}`); 35 | } 36 | const result = methodFn(params); 37 | postMessage({ result: result === undefined ? true : result }); 38 | } catch (error) { 39 | postMessage({ error: String(error) }); 40 | console.error(error); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/react-core/src/types.d.ts: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from './operations/constants/AggregationTypes'; 2 | import { Polygon, MultiPolygon } from 'geojson'; 3 | import { SpatialIndex } from './operations/constants/SpatialIndexTypes'; 4 | 5 | export enum TILE_FORMATS { 6 | MVT = 'mvt', 7 | JSON = 'json', 8 | GEOJSON = 'geojson', 9 | BINARY = 'binary' 10 | } 11 | 12 | export type AggregationFunctions = Record< 13 | AggregationTypes, 14 | ( 15 | values: Record[], 16 | keys?: string | string[], 17 | joinOperation?: AggregationTypes 18 | ) => {} 19 | >; 20 | 21 | export type GroupByFeature = 22 | | { 23 | name: string; 24 | value: number; 25 | }[] 26 | | []; 27 | 28 | export type HistogramFeature = { 29 | min?: number; 30 | max?: number; 31 | data?: number[]; 32 | ticks?: number[]; 33 | }; 34 | 35 | export type ScatterPlotFeature = [number, number][]; 36 | 37 | export type Viewport = [number, number, number, number]; 38 | 39 | export type TileFeatures = { 40 | tiles?: any; // TODO: add proper deck.gl type 41 | viewport?: Viewport; 42 | geometry?: Polygon | MultiPolygon; 43 | uniqueIdProperty?: string; 44 | tileFormat: typeof TILE_FORMATS; 45 | geoColumName?: string; 46 | spatialIndex?: SpatialIndex; 47 | options?: { storeGeometry: boolean } 48 | }; 49 | 50 | export type TileFeaturesResponse = Record[] | []; 51 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgets/utils.js: -------------------------------------------------------------------------------- 1 | import { combineReducers, configureStore } from '@reduxjs/toolkit'; 2 | import * as cartoSlice from '../../../../react-redux/src/slices/cartoSlice'; 3 | 4 | const MOCKED_SOURCE = { 5 | credentials: { 6 | username: 'public', 7 | apiKey: 'default_public' 8 | }, 9 | id: 'sb-data-source', 10 | type: 'sql', 11 | data: 'data' 12 | }; 13 | 14 | let mockedStore = {}; 15 | 16 | function mockReducerManagerCreation(initialReducers) { 17 | const reducers = { ...initialReducers }; 18 | let combinedReducer = Object.keys(reducers).length 19 | ? combineReducers(reducers) 20 | : () => {}; 21 | 22 | return { 23 | reduce: (state, action) => { 24 | return combinedReducer(state, action); 25 | }, 26 | add: (key, reducer) => { 27 | reducers[key] = reducer; 28 | combinedReducer = combineReducers(reducers); 29 | mockedStore.replaceReducer(combinedReducer); 30 | } 31 | }; 32 | } 33 | 34 | export function mockAppStoreConfiguration() { 35 | const reducerManager = mockReducerManagerCreation(); 36 | mockedStore = configureStore({ 37 | reducer: reducerManager.reduce 38 | }); 39 | 40 | mockedStore.reducerManager = reducerManager; 41 | mockedStore.reducerManager.add('carto', cartoSlice.createCartoSlice({})); 42 | mockedStore.dispatch(cartoSlice.addSource(MOCKED_SOURCE)); 43 | 44 | return mockedStore; 45 | } 46 | -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/MultipleSelectField/Filters.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useIntl } from 'react-intl'; 3 | import useImperativeIntl from '../../../hooks/useImperativeIntl'; 4 | import { Checkbox, Link, styled } from '@mui/material'; 5 | import MenuItem from '../MenuItem'; 6 | 7 | const LinkFilter = styled(Link)(({ disabled, theme }) => ({ 8 | display: 'flex', 9 | alignItems: 'center', 10 | gap: theme.spacing(1), 11 | width: '100%', 12 | textAlign: 'initial', 13 | 14 | ...(disabled && { pointerEvents: 'none', color: theme.palette.text.disabled }) 15 | })); 16 | 17 | function Filters({ areAllSelected, areAnySelected, selectAll, selectAllDisabled }) { 18 | const intl = useIntl(); 19 | const intlConfig = useImperativeIntl(intl); 20 | 21 | return ( 22 | 23 | 32 | 37 | {intlConfig.formatMessage({ id: 'c4r.form.selectAll' })} 38 | 39 | 40 | ); 41 | } 42 | 43 | export default Filters; 44 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/HistogramWidgetUI.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, fireEvent, screen } from '../widgets/utils/testUtils'; 3 | import HistogramWidgetUI from '../../src/widgets/HistogramWidgetUI/HistogramWidgetUI'; 4 | import { mockEcharts } from './testUtils'; 5 | 6 | describe('HistogramWidgetUI', () => { 7 | beforeAll(() => { 8 | mockEcharts.init(); 9 | }); 10 | 11 | afterAll(() => { 12 | mockEcharts.destroy(); 13 | }); 14 | 15 | const onSelectedBarsChange = jest.fn(); 16 | 17 | const defaultProps = { 18 | data: [1, 2, 3, 4], 19 | min: 0, 20 | max: 5, 21 | ticks: [0, 1, 2], 22 | onSelectedBarsChange 23 | }; 24 | 25 | const Widget = (props) => ; 26 | 27 | test('all selected', () => { 28 | render(); 29 | expect(screen.getByText(/All/)).toBeInTheDocument(); 30 | }); 31 | 32 | test('re-render with different data', () => { 33 | const { rerender } = render(); 34 | 35 | rerender(); 36 | rerender(); 37 | }); 38 | 39 | test('with selected bars', () => { 40 | render(); 41 | expect(screen.getByText(/2 selected/)).toBeInTheDocument(); 42 | fireEvent.click(screen.getByText(/Clear/)); 43 | expect(onSelectedBarsChange).toHaveBeenCalledTimes(1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /patches/h3-js+3.7.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/h3-js/dist/browser/h3-js.es.js b/node_modules/h3-js/dist/browser/h3-js.es.js 2 | index 5f74d6f..7cdd692 100644 3 | --- a/node_modules/h3-js/dist/browser/h3-js.es.js 4 | +++ b/node_modules/h3-js/dist/browser/h3-js.es.js 5 | @@ -24,9 +24,9 @@ var libh3 = function (libh3) { 6 | var readAsync; 7 | 8 | { 9 | - if (document.currentScript) { 10 | - scriptDirectory = document.currentScript.src; 11 | - } 12 | + // if (document.currentScript) { 13 | + // scriptDirectory = document.currentScript.src; 14 | + // } 15 | 16 | if (scriptDirectory.indexOf("blob:") !== 0) { 17 | scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf("/") + 1); 18 | diff --git a/node_modules/h3-js/dist/browser/h3-js.js b/node_modules/h3-js/dist/browser/h3-js.js 19 | index be0058c..451d04a 100644 20 | --- a/node_modules/h3-js/dist/browser/h3-js.js 21 | +++ b/node_modules/h3-js/dist/browser/h3-js.js 22 | @@ -24,9 +24,9 @@ var libh3 = function (libh3) { 23 | var readAsync; 24 | 25 | { 26 | - if (document.currentScript) { 27 | - scriptDirectory = document.currentScript.src; 28 | - } 29 | + // if (document.currentScript) { 30 | + // scriptDirectory = document.currentScript.src; 31 | + // } 32 | 33 | if (scriptDirectory.indexOf("blob:") !== 0) { 34 | scriptDirectory = scriptDirectory.substr(0, scriptDirectory.lastIndexOf("/") + 1); 35 | -------------------------------------------------------------------------------- /packages/react-basemaps/src/basemaps/basemaps.d.ts: -------------------------------------------------------------------------------- 1 | export enum CartoBasemapsNames { 2 | POSITRON = 'positron', 3 | VOYAGER = 'voyager', 4 | DARK_MATTER = 'dark-matter' 5 | } 6 | 7 | declare enum CartoUrlBasemaps { 8 | POSITRON = 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json', 9 | VOYAGER = 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json', 10 | DARK_MATTER = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json', 11 | } 12 | 13 | export enum GMapsBasemapsNames { 14 | ROADMAP = 'roadmap', 15 | SATELLITE = 'satellite', 16 | HYBRID = 'hybrid', 17 | CUSTOM = 'custom' 18 | } 19 | 20 | export const POSITRON: CartoBasemapsNames.POSITRON; 21 | export const VOYAGER: CartoBasemapsNames.VOYAGER; 22 | export const DARK_MATTER: CartoBasemapsNames.DARK_MATTER; 23 | export const GOOGLE_ROADMAP: GMapsBasemapsNames.ROADMAP; 24 | export const GOOGLE_SATELLITE: GMapsBasemapsNames.SATELLITE; 25 | export const GOOGLE_HYBRID: GMapsBasemapsNames.HYBRID; 26 | export const GOOGLE_CUSTOM: GMapsBasemapsNames.CUSTOM; 27 | 28 | type CartoBasemaps = { 29 | [B in CartoBasemapsNames]: { 30 | type: 'mapbox', 31 | options: { 32 | mapStyle: CartoUrlBasemaps 33 | } 34 | } 35 | } 36 | 37 | type GMapsBasemaps = { 38 | [B in GMapsBasemapsNames]: { 39 | type: 'gmaps', 40 | options: { 41 | mapTypeId: GMapsBasemapsNames 42 | } 43 | } 44 | } 45 | 46 | export const BASEMAPS: CartoBasemaps & GMapsBasemaps; -------------------------------------------------------------------------------- /packages/react-ui/src/components/molecules/MenuList.js: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { MenuList as MuiMenuList, styled } from '@mui/material'; 4 | 5 | const StyledMenuList = styled(MuiMenuList, { 6 | shouldForwardProp: (prop) => !['extended', 'width', 'height'].includes(prop) 7 | })(({ extended, width, height, theme }) => ({ 8 | ...(extended && { 9 | '.MuiMenuItem-root': { 10 | minHeight: theme.spacing(6) 11 | } 12 | }), 13 | '&.MuiList-root': { 14 | ...(width && { 15 | width: width, 16 | minWidth: width 17 | }), 18 | ...(height && { 19 | maxHeight: height 20 | }) 21 | } 22 | })); 23 | 24 | const MenuList = forwardRef( 25 | ({ extended, width, height, children, ...otherProps }, ref) => { 26 | // forwardRef needed to be able to hold a reference, in this way it can be a child for some Mui components, like Tooltip 27 | // https://mui.com/material-ui/guides/composition/#caveat-with-refs 28 | 29 | return ( 30 | 37 | {children} 38 | 39 | ); 40 | } 41 | ); 42 | 43 | MenuList.propTypes = { 44 | extended: PropTypes.bool, 45 | width: PropTypes.string, 46 | height: PropTypes.string 47 | }; 48 | 49 | export default MenuList; 50 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/HistogramWidgetUI/HistogramSkeleton.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Skeleton } from '@mui/material'; 3 | import { 4 | SKELETON_HEIGHT, 5 | SkeletonGraphGrid, 6 | SkeletonThinBarItem 7 | } from '../SkeletonWidgets'; 8 | 9 | const HistogramSkeleton = ({ height }) => { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | }; 33 | 34 | export default HistogramSkeleton; 35 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/legend/LegendCategories.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LegendCategories from '../../../../src/widgets/legend/legend-types/LegendCategories'; 3 | import { IntlProvider } from 'react-intl'; 4 | 5 | const DEFAULT_LEGEND = { 6 | legend: { 7 | labels: ['Category 1', 'Category 2', 'Category 3'], 8 | colors: 'TealGrn' 9 | } 10 | }; 11 | 12 | const options = { 13 | title: 'Widgets/Legends/LegendCategories', 14 | component: LegendCategories, 15 | argTypes: { 16 | legend: {} 17 | }, 18 | parameters: { 19 | docs: { 20 | source: { 21 | type: 'auto' 22 | } 23 | } 24 | } 25 | }; 26 | 27 | export default options; 28 | 29 | const Template = (args) => { 30 | return ( 31 | 32 | 33 | 34 | ); 35 | }; 36 | 37 | export const Default = Template.bind({}); 38 | const DefaultProps = { ...DEFAULT_LEGEND }; 39 | Default.args = DefaultProps; 40 | 41 | export const WithHexColors = Template.bind({}); 42 | const WithHexColorsProps = { 43 | legend: { ...DEFAULT_LEGEND.legend, colors: ['#f00', '#0f0', '#00f'] } 44 | }; 45 | WithHexColors.args = WithHexColorsProps; 46 | 47 | export const WithStrokedColors = Template.bind({}); 48 | const WithStrokedColorsProps = { 49 | legend: { ...DEFAULT_LEGEND.legend, isStrokeColor: true } 50 | }; 51 | WithStrokedColors.args = WithStrokedColorsProps; 52 | -------------------------------------------------------------------------------- /packages/react-widgets/src/widgets/TimeSeriesWidget.d.ts: -------------------------------------------------------------------------------- 1 | import { AggregationTypes, GroupDateTypes } from '@carto/react-core'; 2 | import { CommonWidgetProps, MonoColumnWidgetProps } from '../types'; 3 | 4 | export interface TimeseriesWidgetSerie { 5 | operation: AggregationTypes; 6 | operationColumn?: string; 7 | }; 8 | 9 | export type TimeSeriesWidgetProps = { 10 | operationColumn?: string; 11 | series?: TimeseriesWidgetSerie[]; 12 | 13 | stepSize: GroupDateTypes; 14 | stepMultiplier?: number; 15 | stepSizeOptions?: string[]; 16 | 17 | splitByCategory?: string; 18 | splitByCategoryLimit?: number; 19 | splitByCategoryValues?: string[]; 20 | 21 | chartType?: string; 22 | timeAxisSplitNumber?: number; 23 | tooltip?: boolean; 24 | tooltipFormatter?: Function; 25 | formatter?: Function; 26 | 27 | filterable?: boolean; 28 | 29 | height?: string; 30 | fitHeihgt?: boolean; 31 | stableHeight?: boolean; 32 | showControls?: boolean; 33 | showLegend?: boolean; 34 | 35 | isPlaying?: boolean; 36 | isPaused?: boolean; 37 | timeWindow?: number[]; 38 | 39 | onPlay?: () => void; 40 | onPause?: () => void; 41 | onStop?: () => void; 42 | onTimelineUpdate?: (position: number) => void; 43 | onTimeWindowUpdate?: (timeWindow: [number, number]) => void; 44 | } & CommonWidgetProps & 45 | MonoColumnWidgetProps; 46 | 47 | declare const TimeSeriesWidget: (props: TimeSeriesWidgetProps) => JSX.Element; 48 | export default TimeSeriesWidget; 49 | -------------------------------------------------------------------------------- /packages/react-ui/src/custom-components/AnimatedNumber.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import useAnimatedNumber from '../hooks/useAnimatedNumber'; 4 | 5 | const EMPTY_OBJECT = {}; 6 | 7 | /** 8 | * Renders a widget 9 | * @param {Object} props 10 | * @param {boolean} props.enabled 11 | * @param {number} props.value 12 | * @param {{ duration?: number; animateOnMount?: boolean; initialValue?: number }} [props.options] 13 | * @param {(n: number) => React.ReactNode} [props.formatter] 14 | */ 15 | function AnimatedNumber({ 16 | enabled = true, 17 | value = 0, 18 | options = EMPTY_OBJECT, 19 | formatter = null 20 | }) { 21 | const defaultOptions = { 22 | animateOnMount: true, 23 | disabled: enabled === false || value === null || value === undefined 24 | }; 25 | const animated = useAnimatedNumber(value || 0, { ...defaultOptions, ...options }); 26 | return {formatter ? formatter(animated) : animated}; 27 | } 28 | 29 | AnimatedNumber.displayName = 'AnimatedNumber'; 30 | 31 | export const animationOptionsPropTypes = PropTypes.shape({ 32 | duration: PropTypes.number, 33 | animateOnMount: PropTypes.bool, 34 | initialValue: PropTypes.number 35 | }); 36 | 37 | AnimatedNumber.propTypes = { 38 | enabled: PropTypes.bool, 39 | value: PropTypes.number.isRequired, 40 | options: animationOptionsPropTypes, 41 | formatter: PropTypes.func 42 | }; 43 | 44 | export default AnimatedNumber; 45 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/WrapperWidgetUI.d.ts: -------------------------------------------------------------------------------- 1 | import { BoxProps, TooltipProps } from '@mui/material'; 2 | import { ReactNode } from 'react'; 3 | 4 | export type WrapperWidgetAction = { 5 | id: string; 6 | 7 | icon: ReactNode; 8 | 9 | action: () => void; 10 | 11 | /// Aria label of action 12 | label?: string; 13 | 14 | // Optional tooltip 15 | tooltip?: { text: string; placement?: TooltipProps['placement'] }; 16 | }; 17 | 18 | export type WrapperWidgetOption = { 19 | id: string; 20 | 21 | // Displayed label of action 22 | name: string; 23 | selected?: boolean; 24 | action: () => void; 25 | }; 26 | 27 | export type WrapperWidgetUIProps = { 28 | title: string; 29 | 30 | expandable?: boolean; 31 | expanded?: boolean; 32 | onExpandedChange?: (expanded: boolean) => void; 33 | 34 | isLoading?: boolean; 35 | disabled?: boolean; 36 | 37 | headerItems?: ReactNode; 38 | actions?: WrapperWidgetAction[]; 39 | options?: WrapperWidgetOption[]; 40 | 41 | /** Override defaulr margin (CSS margin value). */ 42 | margin?: number | string; 43 | 44 | /** Optional footer added inside content box after widget itself. */ 45 | footer?: ReactNode; 46 | 47 | /** Extra props to Box that wraps content ontent of widget (widget itself and footer)) */ 48 | contentProps?: BoxProps; 49 | 50 | children?: ReactNode; 51 | }; 52 | 53 | declare const WrapperWidgetUI: (props: WrapperWidgetUIProps) => JSX.Element; 54 | export default WrapperWidgetUI; 55 | -------------------------------------------------------------------------------- /packages/react-widgets/src/widgets/utils/propTypesFns.js: -------------------------------------------------------------------------------- 1 | import { AggregationTypes } from '@carto/react-core'; 2 | 3 | export const columnAggregationOn = (columnPropName) => (props, propName) => { 4 | if (Array.isArray(props[columnPropName]) && props[columnPropName].length >= 2) { 5 | if (!props[propName]) { 6 | return new Error( 7 | `Prop ${propName} must be defined if ${columnPropName} is an array` 8 | ); 9 | } 10 | if (Object.values(AggregationTypes).indexOf(props[propName]) === -1) { 11 | return new Error(`Prop ${propName} must be a valid aggregation operator`); 12 | } 13 | } 14 | }; 15 | 16 | // FormulaWidget 17 | export const checkFormulaColumn = (props, propName) => { 18 | const propValue = props[propName]; 19 | if (props.operation === AggregationTypes.CUSTOM) { 20 | return; 21 | } 22 | 23 | const isValidString = !!propValue && typeof propValue === 'string'; 24 | const isValidArray = Array.isArray(propValue) && propValue.length; 25 | 26 | const isValid = isValidString || isValidArray; 27 | 28 | const validationError = new Error(`Prop ${propName} must be a string or an array`); 29 | 30 | if (props.operation === AggregationTypes.COUNT) { 31 | if (propValue && !isValid) { 32 | return validationError; 33 | } 34 | } else { 35 | if (!propValue) { 36 | return new Error(`Prop ${propName} must be defined`); 37 | } 38 | 39 | if (!isValid) { 40 | return validationError; 41 | } 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /scripts/mergeCoverage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script merges the coverage reports from different packages into a single one, 3 | * inside the "coverage" folder 4 | * 5 | * Adapted from: https://github.com/rafaelalmeidatk/TIL/issues/2 6 | */ 7 | 8 | const { execSync } = require("child_process"); 9 | const fs = require("fs-extra"); 10 | 11 | const REPORTS_FOLDER = "reports"; 12 | const FINAL_OUTPUT_FOLDER = "coverage"; 13 | 14 | const run = commands => { 15 | commands.forEach(command => execSync(command, { stdio: "inherit" })); 16 | }; 17 | 18 | // Create the reports folder and move the reports from cypress and jest inside it 19 | fs.emptyDirSync(REPORTS_FOLDER); 20 | 21 | const packages = ['react-api', 'react-auth', 'react-basemaps', 'react-core', 'react-redux', 'react-ui', 'react-widgets']; 22 | 23 | packages.forEach((packageName) => 24 | fs.copyFileSync( 25 | `packages/${packageName}/coverage/coverage-final.json`, 26 | `${REPORTS_FOLDER}/${packageName}-coverage.json` 27 | ) 28 | ); 29 | 30 | fs.emptyDirSync(".nyc_output"); 31 | fs.emptyDirSync(FINAL_OUTPUT_FOLDER); 32 | 33 | // Run "nyc merge" inside the reports folder, merging the different coverage files into one, 34 | // then generate the final report on the coverage folder 35 | run([ 36 | // "nyc merge" will create a "coverage.json" file on the root, we move it to .nyc_output 37 | `npx nyc merge ${REPORTS_FOLDER} && mv coverage.json .nyc_output/out.json`, 38 | `npx nyc report --reporter lcov --reporter html --report-dir ${FINAL_OUTPUT_FOLDER}` 39 | ]); 40 | -------------------------------------------------------------------------------- /packages/react-api/__tests__/api/lds.test.js: -------------------------------------------------------------------------------- 1 | import { ldsGeocode } from '../../src/api/lds'; 2 | import { API_VERSIONS } from '../../src/types'; 3 | 4 | const sampleCredentialsV3 = { 5 | apiVersion: API_VERSIONS.V3, 6 | accessToken: 'thisIsTheTestToken', 7 | apiBaseUrl: 'https://api.com/' 8 | }; 9 | 10 | const someCoordinates = { 11 | latitude: 42.360278, 12 | longitude: -71.057778 13 | }; 14 | 15 | describe('lds', () => { 16 | describe('ldsDecode', () => { 17 | test('should send proper requests', async () => { 18 | const fetchMock = (global.fetch = jest.fn().mockImplementation(async () => { 19 | return { 20 | ok: true, 21 | json: async () => [{ value: [someCoordinates] }] 22 | }; 23 | })); 24 | 25 | const abortController = new AbortController(); 26 | expect( 27 | await ldsGeocode({ 28 | credentials: sampleCredentialsV3, 29 | address: 'boston', 30 | country: 'US', 31 | limit: 4, 32 | opts: { 33 | abortController: abortController 34 | } 35 | }) 36 | ).toEqual([someCoordinates]); 37 | 38 | expect(fetchMock).toBeCalledWith( 39 | 'https://api.com//v3/lds/geocoding/geocode?client=c4react&address=boston&country=US&limit=4', 40 | { 41 | headers: { 42 | Authorization: `Bearer ${sampleCredentialsV3.accessToken}` 43 | }, 44 | signal: abortController.signal 45 | } 46 | ); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/legend/LegendRamp.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LegendRamp from '../../../../src/widgets/legend/legend-types/LegendRamp'; 3 | 4 | const DEFAULT_LEGEND = { 5 | legend: { 6 | labels: [0, 200], 7 | colors: 'TealGrn' 8 | } 9 | }; 10 | 11 | const DEFAULT_LEGEND_WITH_FORMATTED_LABELS = { 12 | legend: { 13 | labels: [ 14 | { value: 0, label: '0 km' }, 15 | { value: 100, label: '100 km' }, 16 | { value: 200, label: '200 km' } 17 | ], 18 | colors: 'TealGrn' 19 | } 20 | }; 21 | 22 | const options = { 23 | title: 'Widgets/Legends/LegendRamp', 24 | component: LegendRamp, 25 | argTypes: { 26 | legend: {} 27 | }, 28 | parameters: { 29 | docs: { 30 | source: { 31 | type: 'auto' 32 | } 33 | } 34 | } 35 | }; 36 | 37 | export default options; 38 | 39 | const Template = (args) => { 40 | return ; 41 | }; 42 | 43 | export const Discontinuous = Template.bind({}); 44 | Discontinuous.args = { ...DEFAULT_LEGEND }; 45 | 46 | export const DiscontinuousWithFormattedLabels = Template.bind({}); 47 | DiscontinuousWithFormattedLabels.args = { ...DEFAULT_LEGEND_WITH_FORMATTED_LABELS }; 48 | 49 | export const Continuous = Template.bind({}); 50 | Continuous.args = { ...DEFAULT_LEGEND, isContinuous: true }; 51 | 52 | export const ContinuousWithFormattedLabels = Template.bind({}); 53 | ContinuousWithFormattedLabels.args = { 54 | ...DEFAULT_LEGEND_WITH_FORMATTED_LABELS, 55 | isContinuous: true 56 | }; 57 | -------------------------------------------------------------------------------- /packages/react-ui/__tests__/widgets/ComparativePieWidgetUI.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '../widgets/utils/testUtils'; 3 | import ComparativePieWidgetUI from '../../src/widgets/comparative/ComparativePieWidgetUI'; 4 | import { mockEcharts } from './testUtils'; 5 | 6 | const PIE_DATA_PROPS = { 7 | names: ['name 1', 'name 2'], 8 | data: [ 9 | [ 10 | { name: 'data 1', value: 40 }, 11 | { name: 'data 2', value: 60 } 12 | ], 13 | [ 14 | { name: 'data 1', value: 30 }, 15 | { name: 'data 2', value: 70 } 16 | ] 17 | ], 18 | labels: [ 19 | ['label 1', 'label 2'], 20 | ['label 1', 'label 2'] 21 | ], 22 | colors: [ 23 | ['#6732a8', '#32a852'], 24 | ['#a83232', '#ff9900'] 25 | ] 26 | }; 27 | 28 | describe('ComparativePieWidgetUI', () => { 29 | beforeAll(() => { 30 | mockEcharts.init(); 31 | }); 32 | 33 | afterAll(() => { 34 | mockEcharts.destroy(); 35 | }); 36 | 37 | const Widget = (props) => ; 38 | 39 | test('renders correctly', () => { 40 | render(); 41 | }); 42 | 43 | test('with one selected category', () => { 44 | render(); 45 | }); 46 | 47 | test('rerenders with different selected category', () => { 48 | const { rerender } = render(); 49 | 50 | rerender(); 51 | rerender(); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/atoms/HelperText.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormControl, FormHelperText, OutlinedInput } from '@mui/material'; 3 | 4 | const options = { 5 | title: 'Atoms/Helper Text', 6 | component: FormHelperText, 7 | argTypes: { 8 | disabled: { 9 | control: { 10 | type: 'boolean' 11 | } 12 | }, 13 | error: { 14 | control: { 15 | type: 'boolean' 16 | } 17 | }, 18 | label: { 19 | control: { 20 | type: 'text' 21 | } 22 | } 23 | }, 24 | parameters: { 25 | design: { 26 | type: 'figma', 27 | url: 'https://www.figma.com/file/nmaoLeo69xBJCHm9nc6lEV/C4R-Components?node-id=1534-33807&t=dVNCJzz6IduwAMHg-0' 28 | }, 29 | status: { 30 | type: 'validated' 31 | } 32 | } 33 | }; 34 | export default options; 35 | 36 | const Template = ({ label, ...args }) => { 37 | return {label}; 38 | }; 39 | 40 | const CompositionTemplate = ({ label, ...args }) => { 41 | return ( 42 | 43 | 44 | {label} 45 | 46 | ); 47 | }; 48 | 49 | const commonArgs = { 50 | label: 'Helper text to be placed below an input' 51 | }; 52 | 53 | export const Playground = Template.bind({}); 54 | Playground.args = { ...commonArgs }; 55 | 56 | export const Composition = CompositionTemplate.bind({}); 57 | Composition.args = { ...commonArgs }; 58 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/utils/utils.js: -------------------------------------------------------------------------------- 1 | function makeSpaces(length) { 2 | return ' '.repeat(length); 3 | } 4 | 5 | function addCommaRecognizer(p) { 6 | return p.toString().replace(/,/g, '_commaInArray'); 7 | } 8 | 9 | function parseObject(obj, spaces) { 10 | return JSON.stringify(obj) 11 | .replace(/{/g, `{\n${makeSpaces(spaces)}`) 12 | .replace(/,/g, `\n${makeSpaces(spaces)}`); 13 | } 14 | 15 | function makeValueTransformation(prop) { 16 | if (Array.isArray(prop)) { 17 | if (!prop.length) { 18 | return '{[]}'; 19 | } 20 | 21 | if (prop.every((p) => typeof p === 'object')) { 22 | return `${JSON.stringify(prop)}`; 23 | } 24 | 25 | if (prop.every((p) => typeof p === 'string')) { 26 | prop = prop.map((p) => `'${p}'`); 27 | } 28 | 29 | return `{[${addCommaRecognizer(prop)}]}`; 30 | } 31 | 32 | if (typeof prop === 'object') { 33 | return `{${parseObject(prop, 4)}}`.replace(/}}/g, '\n }}'); 34 | } 35 | 36 | if (typeof prop === 'string') { 37 | return `'${prop}'`; 38 | } 39 | 40 | return `{${prop}}`; 41 | } 42 | 43 | export function buildReactPropsAsString(props, componentName) { 44 | const transformedProps = Object.entries(props).map( 45 | ([k, v]) => k + '=' + makeValueTransformation(v) 46 | ); 47 | 48 | return { 49 | docs: { 50 | source: { 51 | code: `<${componentName}\n ${transformedProps 52 | .join() 53 | .replace(/,/g, '\n ') 54 | .replace(/_commaInArray/g, ', ')}\n/>` 55 | } 56 | } 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /packages/react-api/src/api/lds.js: -------------------------------------------------------------------------------- 1 | import { _getClient } from '@carto/react-core'; 2 | import { checkCredentials, makeCall } from './common'; 3 | 4 | /** 5 | * Execute a LDS geocoding service geocode request. 6 | * 7 | * @param { object } props 8 | * @param { string } props.address - searched address to be executed 9 | * @param { string= } props.country - optional, limit search scope to country as ISO-3166 alpha-2 code, example, ES, DE 10 | * @param { number= } props.limit - optional, limit of ansewers 11 | * @param { Object } props.credentials - CARTO user credentials 12 | * @param { string } props.credentials.accessToken - CARTO 3 access token 13 | * @param { string } props.credentials.apiBaseUrl - CARTO 3 api server URL 14 | * @param { Object= } props.opts - Additional options for the HTTP request 15 | */ 16 | export async function ldsGeocode({ credentials, address, country, limit, opts }) { 17 | checkCredentials(credentials); 18 | 19 | if (!address) { 20 | throw new Error('ldsGeocode: No address provided'); 21 | } 22 | 23 | const url = new URL(`${credentials.apiBaseUrl}/v3/lds/geocoding/geocode`); 24 | url.searchParams.set('client', _getClient()); 25 | url.searchParams.set('address', address); 26 | if (country) { 27 | url.searchParams.set('country', country); 28 | } 29 | if (limit) { 30 | url.searchParams.set('limit', String(limit)); 31 | } 32 | 33 | let data = await makeCall({ url, credentials, opts }); 34 | 35 | if (Array.isArray(data)) { 36 | data = data[0]; 37 | } 38 | 39 | return data.value; 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-widgets/src/models/ScatterPlotModel.js: -------------------------------------------------------------------------------- 1 | import { _executeModel } from '@carto/react-api'; 2 | import { Methods, executeTask } from '@carto/react-workers'; 3 | import { normalizeObjectKeys, wrapModelCall } from './utils'; 4 | 5 | // Make sure this is sync with the same constant in cloud-native/maps-api 6 | export const HARD_LIMIT = 500; 7 | 8 | export function getScatter(props) { 9 | return wrapModelCall(props, fromLocal, fromRemote); 10 | } 11 | 12 | function fromLocal(props) { 13 | const { source, xAxisColumn, xAxisJoinOperation, yAxisColumn, yAxisJoinOperation } = 14 | props; 15 | 16 | return executeTask(source.id, Methods.FEATURES_SCATTERPLOT, { 17 | filters: source.filters, 18 | filtersLogicalOperator: source.filtersLogicalOperator, 19 | xAxisColumn, 20 | xAxisJoinOperation, 21 | yAxisColumn, 22 | yAxisJoinOperation 23 | }); 24 | } 25 | 26 | function formatResult(res) { 27 | return res.map(({ x, y }) => [x, y]); 28 | } 29 | 30 | function fromRemote(props) { 31 | const { source, spatialFilter, abortController, ...params } = props; 32 | const { xAxisColumn, xAxisJoinOperation, yAxisColumn, yAxisJoinOperation } = params; 33 | 34 | return _executeModel({ 35 | model: 'scatterplot', 36 | source, 37 | spatialFilter, 38 | params: { 39 | xAxisColumn, 40 | xAxisJoinOperation, 41 | yAxisColumn, 42 | yAxisJoinOperation, 43 | limit: HARD_LIMIT 44 | }, 45 | opts: { abortController } 46 | }) 47 | .then((res) => normalizeObjectKeys(res.rows)) 48 | .then(formatResult); 49 | } 50 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/foundations/TypographyGuide.stories.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/addon-docs'; 2 | 3 | 4 | 5 | # Typography 6 | 7 | ## C4R component 8 | 9 | We have our own [Typography](https://github.com/CartoDB/carto-react/blob/master/packages/react-ui/src/components/atoms/Typography.js) component that uses `Mui Typography` and extends it with some styling props: 10 | 11 | - weight 12 | - italic 13 | 14 | This way we can be more flexible regarding text styles without adding too many variants to the theme, an avoiding the same customizations over and over again. 15 | 16 | In short, instead of Mui Typography, the component you should use to add text is this one: 17 | `react-ui/src/components/atoms/Typography` 18 | 19 | For external use: `import { Typography } from '@carto/react-ui';`. 20 | 21 | ## Responsive 22 | 23 | `responsiveFontSizes` simplified due we want to resize only a few variants through the theme. 24 | 25 | ### New variants 26 | 27 | - `overlineDelicate` 28 | - `code1` 29 | - `code2` 30 | - `code3` 31 | 32 | ### Replaced variants 33 | 34 | Replaced variants due they were so specific to some components, these are: 35 | 36 | - `charts`: replaced by `theme.palette.overline` + `weight='strong'` 37 | 38 | ### Font families 39 | 40 | For external use: `Open Sans` and `Montserrat` families have been replaced by `Inter` and `Overpass Mono`, you have an example of this in the [`preview-head.html`](https://github.com/CartoDB/carto-react/blob/master/packages/react-ui/storybook/.storybook/preview-head.html) file. 41 | -------------------------------------------------------------------------------- /packages/react-api/__tests__/api/tilejson.test.js: -------------------------------------------------------------------------------- 1 | import { getTileJson } from '../../src/api/tilejson'; 2 | import { MAP_TYPES, API_VERSIONS } from '../../src/types'; 3 | 4 | const mockedVectorTilesetSource = jest.fn(); 5 | 6 | jest.mock('@deck.gl/carto', () => ({ 7 | ...jest.requireActual('@deck.gl/carto'), 8 | vectorTilesetSource: (props) => { 9 | mockedVectorTilesetSource(props); 10 | return Promise.resolve({}); 11 | } 12 | })); 13 | 14 | const TEST_CONNECTION = '__test_connection__'; 15 | const TEST_TILESET = '__test_tileset__'; 16 | const TEST_API_KEY = '__test_api_key__'; 17 | 18 | describe('tilejson', () => { 19 | describe('getTileJson', () => { 20 | test('should return a tilejson', async () => { 21 | const source = { 22 | type: MAP_TYPES.TILESET, 23 | data: TEST_TILESET, 24 | connection: TEST_CONNECTION, 25 | credentials: { 26 | accessToken: TEST_API_KEY, 27 | apiVersion: API_VERSIONS.V3, 28 | apiBaseUrl: 'https://gcp-us-east1.api.carto.com' 29 | } 30 | }; 31 | 32 | const tilejson = await getTileJson({ source }); 33 | 34 | expect(mockedVectorTilesetSource).toBeCalledWith({ 35 | connectionName: '__test_connection__', 36 | apiBaseUrl: 'https://gcp-us-east1.api.carto.com', 37 | accessToken: '__test_api_key__', 38 | clientId: 'carto-for-react', // hardcoded as no neeed to export CLIENT_ID from '@carto/react-api/api/common'; 39 | tableName: '__test_tileset__' 40 | }); 41 | 42 | expect(tilejson).toBeDefined(); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/icons/CartoIcons.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid } from '@mui/material'; 3 | import { Typography } from '@carto/react-ui'; 4 | import { icons } from '../../../src/assets'; 5 | import { GridVerticalContent } from '../../utils/storyStyles'; 6 | 7 | const options = { 8 | title: 'Icons/CARTO Icons', 9 | argTypes: { 10 | fontSize: { 11 | control: { 12 | type: 'select', 13 | options: ['small', 'medium', 'large'] 14 | } 15 | }, 16 | color: { 17 | control: { 18 | type: 'select', 19 | options: [ 20 | 'action', 21 | 'disabled', 22 | 'primary', 23 | 'secondary', 24 | 'error', 25 | 'info', 26 | 'success', 27 | 'warning', 28 | 'default' 29 | ] 30 | } 31 | } 32 | }, 33 | parameters: { 34 | design: { 35 | type: 'figma', 36 | url: 'https://www.figma.com/file/Yj97O00yGzMg1ULcA0WEfl/CARTO-Icons?node-id=8816-2893&t=b1zTHwFjHKGCo8BC-0' 37 | } 38 | } 39 | }; 40 | export default options; 41 | 42 | const Template = ({ ...args }) => { 43 | const iconsList = Object.entries(icons); 44 | 45 | return ( 46 | 47 | {iconsList.map(([key, Icon]) => ( 48 | 49 | 50 | {key} 51 | 52 | ))} 53 | 54 | ); 55 | }; 56 | 57 | export const Playground = Template.bind({}); 58 | -------------------------------------------------------------------------------- /packages/react-ui/src/widgets/legend/legend-types/LegendIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ICON_SIZE_MEDIUM } from '../../../theme/themeConstants'; 4 | import { 5 | LegendIconImageWrapper, 6 | LegendIconWrapper, 7 | LegendVariableList 8 | } from '../LegendWidgetUI.styles'; 9 | import LegendLayerTitle from '../LegendLayerTitle'; 10 | 11 | const DEFAULT_LEGEND = { 12 | labels: [], 13 | icons: [] 14 | }; 15 | 16 | /** 17 | * @param {object} props 18 | * @param {import('../LegendWidgetUI').LegendLayerVariableBase & import('../LegendWidgetUI').LegendIcons} props.legend - legend variable data. 19 | * @returns {React.ReactNode} 20 | */ 21 | function LegendIcon({ legend = DEFAULT_LEGEND }) { 22 | const { labels = [], icons = [] } = legend; 23 | return ( 24 | 25 | {labels.map((label, idx) => ( 26 | 27 | 28 | {label} 29 | 30 | 35 | 36 | ))} 37 | 38 | ); 39 | } 40 | 41 | LegendIcon.propTypes = { 42 | legend: PropTypes.shape({ 43 | labels: PropTypes.arrayOf(PropTypes.string), 44 | icons: PropTypes.arrayOf(PropTypes.string) 45 | }).isRequired 46 | }; 47 | 48 | export default LegendIcon; 49 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/assets/doc.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /packages/react-widgets/src/hooks/useStats.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { _getStats } from '@carto/react-api/'; 3 | import { InvalidColumnError } from '@carto/react-core/'; 4 | import useWidgetSource from './useWidgetSource'; 5 | import { DEFAULT_INVALID_COLUMN_ERR } from '../widgets/utils/constants'; 6 | 7 | /** 8 | * Hook to obtain column stats 9 | * @param {object} props 10 | * @param {string} props.id - ID for the widget instance. 11 | * @param {string} props.column - Name of the data source's column to get the stats. 12 | * @param {string} props.dataSource - ID of the data source to get the stats from. 13 | * @param {boolean} props.customStats - If we are using custom stats is not necessary to do the request 14 | * @param {Function} [props.onError] - Function to handle error messages from the widget. 15 | */ 16 | export default function useStats({ id, column, dataSource, customStats, onError }) { 17 | const [stats, setStats] = useState(); 18 | const [warning, setWarning] = useState(''); 19 | const source = useWidgetSource({ dataSource, id }); 20 | 21 | useEffect(() => { 22 | if (!customStats && source) { 23 | setWarning(''); 24 | 25 | _getStats({ column, source }) 26 | .then((res) => { 27 | setStats(res); 28 | }) 29 | .catch((err) => { 30 | if (InvalidColumnError.is(err)) { 31 | setWarning(DEFAULT_INVALID_COLUMN_ERR); 32 | } else if (onError) { 33 | onError(err); 34 | } 35 | }); 36 | } 37 | }, [column, source, onError, customStats]); 38 | 39 | return { stats, warning }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useAnimatedNumber.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | import { animateValue } from '../widgets/utils/animations'; 3 | 4 | /** 5 | * React hook to handle animating value changes over time, abstracting the necesary state, refs and effects 6 | * @param {number} value 7 | * @param {{ disabled?: boolean; duration?: number; animateOnMount?: boolean; initialValue?: number; }} [options] 8 | */ 9 | export default function useAnimatedNumber(value, options = {}) { 10 | const { disabled, duration, animateOnMount, initialValue = 0 } = options; 11 | 12 | /** @type {any} */ 13 | const requestAnimationFrameRef = useRef(); 14 | 15 | // if we want to run the animation on mount, we set the starting value of the animated number as 0 (or the number in `initialValue`) and animate to the target value from there 16 | const [animatedValue, setAnimatedValue] = useState(() => 17 | animateOnMount ? initialValue : value 18 | ); 19 | 20 | useEffect(() => { 21 | if (!disabled) { 22 | animateValue({ 23 | start: animatedValue, 24 | end: value, 25 | duration: duration || 500, // 500ms 26 | drawFrame: (val) => setAnimatedValue(val), 27 | requestRef: requestAnimationFrameRef 28 | }); 29 | } else { 30 | setAnimatedValue(value); 31 | } 32 | 33 | return () => { 34 | // eslint-disable-next-line react-hooks/exhaustive-deps 35 | cancelAnimationFrame(requestAnimationFrameRef.current); 36 | }; 37 | // eslint-disable-next-line react-hooks/exhaustive-deps 38 | }, [value, disabled, duration]); 39 | 40 | return animatedValue; 41 | } 42 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/RangeWidgetUI.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import RangeWidgetUI from '../../../src/widgets/RangeWidgetUI/RangeWidgetUI'; 3 | import { Label, ThinContainer } from '../../utils/storyStyles'; 4 | import { IntlProvider } from 'react-intl'; 5 | 6 | const options = { 7 | title: 'Widgets/RangeWidgetUI', 8 | component: RangeWidgetUI, 9 | parameters: { 10 | docs: { 11 | source: { 12 | type: 'auto' 13 | } 14 | } 15 | } 16 | }; 17 | 18 | export default options; 19 | 20 | const Widget = (props) => ( 21 | 22 | 23 | 24 | ); 25 | 26 | const Template = (args) => { 27 | return ; 28 | }; 29 | 30 | const LoadingTemplate = (args) => { 31 | if (args.series && !Array.isArray(args.series)) { 32 | args.series = []; 33 | } 34 | 35 | return ( 36 | <> 37 | 40 | 41 | 42 | 43 | 44 | 47 | 48 | 49 | ); 50 | }; 51 | 52 | const data = { 53 | data: [400, 500], 54 | min: 0, 55 | max: 1000, 56 | limits: [300, 950] 57 | }; 58 | 59 | export const Default = Template.bind({}); 60 | const DefaultProps = { ...data }; 61 | Default.args = DefaultProps; 62 | 63 | export const Loading = LoadingTemplate.bind({}); 64 | const LoadingProps = { ...data, isLoading: true }; 65 | Loading.args = LoadingProps; 66 | -------------------------------------------------------------------------------- /packages/react-workers/src/workerPool.js: -------------------------------------------------------------------------------- 1 | import featuresWorker from './workers/features.worker'; 2 | 3 | const pool = {}; 4 | 5 | export function executeTask(source, method, params) { 6 | return new Promise((resolve, reject) => { 7 | const worker = getWorker(source); 8 | worker.tasks.push({ 9 | method, 10 | params, 11 | resolve, 12 | reject 13 | }); 14 | if (worker.tasks.length === 1) { 15 | resolveWorkerTasks(worker); 16 | } 17 | }); 18 | } 19 | 20 | export function removeWorker(source) { 21 | if (pool[source]) { 22 | const removeSourceError = new Error(); 23 | removeSourceError.name = 'AbortError'; 24 | pool[source].tasks.forEach((t) => t.reject(removeSourceError)); 25 | pool[source].worker.terminate(); 26 | delete pool[source]; 27 | } 28 | } 29 | 30 | function getWorker(source) { 31 | if (!pool[source]) { 32 | pool[source] = { 33 | worker: new featuresWorker(), 34 | tasks: [] 35 | }; 36 | onmessage(pool[source]); 37 | onerror(pool[source]); 38 | } 39 | return pool[source]; 40 | } 41 | 42 | function onmessage(w) { 43 | w.worker.onmessage = ({ data: { result } }) => { 44 | const task = w.tasks.shift(); 45 | task.resolve(result); 46 | resolveWorkerTasks(w); 47 | }; 48 | } 49 | 50 | function onerror(w) { 51 | w.worker.onerror = (err) => { 52 | const task = w.tasks.shift(); 53 | resolveWorkerTasks(w); 54 | task.reject(err); 55 | }; 56 | } 57 | 58 | function resolveWorkerTasks(w) { 59 | if (w.tasks.length > 0) { 60 | const { method, params } = w.tasks[0]; 61 | w.worker.postMessage({ method, ...params }); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/react-ui/storybook/stories/widgetsUI/ComparativeFormulaWidgetUI.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ComparativeFormulaWidgetUI from '../../../src/widgets/comparative/ComparativeFormulaWidgetUI/ComparativeFormulaWidgetUI'; 3 | import { buildReactPropsAsString } from '../../utils/utils'; 4 | import { Label, ThinContainer } from '../../utils/storyStyles'; 5 | 6 | const options = { 7 | title: 'Widgets/ComparativeFormulaWidgetUI', 8 | component: ComparativeFormulaWidgetUI 9 | }; 10 | 11 | export default options; 12 | 13 | const Template = (args) => ; 14 | 15 | const LoadingTemplate = (args) => { 16 | return ( 17 | <> 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | const sampleProps = { 34 | data: [ 35 | { prefix: '$', suffix: ' sales', label: 'label 1', value: 1245 }, 36 | { prefix: '$', suffix: ' sales', label: 'label 2', value: 3435.9 } 37 | ], 38 | colors: ['#ff9900'] 39 | }; 40 | 41 | export const Default = Template.bind({}); 42 | Default.args = sampleProps; 43 | Default.parameters = buildReactPropsAsString(sampleProps, 'ComparativeFormulaWidgetUI'); 44 | 45 | export const Loading = LoadingTemplate.bind({}); 46 | Loading.args = { ...sampleProps, isLoading: true }; 47 | Loading.parameters = buildReactPropsAsString(sampleProps, 'ComparativeFormulaWidgetUI'); 48 | -------------------------------------------------------------------------------- /packages/react-widgets/src/index.js: -------------------------------------------------------------------------------- 1 | export { default as CategoryWidget } from './widgets/CategoryWidget'; 2 | export { default as FormulaWidget } from './widgets/FormulaWidget'; 3 | export { default as GeocoderWidget } from './widgets/GeocoderWidget'; 4 | export { default as HistogramWidget } from './widgets/HistogramWidget'; 5 | export { default as PieWidget } from './widgets/PieWidget'; 6 | export { default as LegendWidget } from './widgets/LegendWidget'; 7 | export { default as ScatterPlotWidget } from './widgets/ScatterPlotWidget'; 8 | export { default as TimeSeriesWidget } from './widgets/TimeSeriesWidget'; 9 | export { default as BarWidget } from './widgets/BarWidget'; 10 | export { default as FeatureSelectionWidget } from './widgets/FeatureSelectionWidget'; 11 | export { default as TableWidget } from './widgets/TableWidget'; 12 | export { default as RangeWidget } from './widgets/RangeWidget'; 13 | export { default as WidgetWithAlert } from './widgets/utils/WidgetWithAlert'; 14 | export { 15 | getFormula, 16 | getHistogram, 17 | getCategories, 18 | geocodeStreetPoint, 19 | getScatter, 20 | getTable 21 | } from './models'; 22 | export { default as useSourceFilters } from './hooks/useSourceFilters'; 23 | export { default as FeatureSelectionLayer } from './layers/FeatureSelectionLayer'; 24 | export { 25 | default as useGeocoderWidgetController, 26 | setGeocoderResult 27 | } from './hooks/useGeocoderWidgetController'; 28 | export { WidgetStateType } from './hooks/useWidgetFetch'; 29 | export { 30 | isRemoteCalculationSupported as _isRemoteCalculationSupported, 31 | sourceAndFiltersToSQL as _sourceAndFiltersToSQL, 32 | getSqlEscapedSource as _getSqlEscapedSource 33 | } from './models/utils'; 34 | -------------------------------------------------------------------------------- /packages/react-core/__tests__/utils/transformToTileCoords.test.js: -------------------------------------------------------------------------------- 1 | import transformToTileCoords from '../../src/utils/transformToTileCoords'; 2 | 3 | const GEOMETRY_AS_WGS84 = { 4 | type: 'Polygon', 5 | coordinates: [ 6 | [ 7 | [-90.5712890625, 43.389081939117496], 8 | [-97.6025390625, 40.613952441166596], 9 | [-87.9345703125, 36.98500309285596], 10 | [-82.79296874999999, 37.92686760148135], 11 | [-83.4521484375, 40.27952566881291], 12 | [-84.990234375, 42.19596877629178], 13 | [-89.6484375, 40.01078714046552], 14 | [-90.5712890625, 43.389081939117496] 15 | ] 16 | ] 17 | }; 18 | 19 | const TILE_2_3_3_BBOX = { west: -90, north: 40.97989806962013, east: -45, south: 0 }; 20 | 21 | const GEOMETRY_AS_TILE_COORDS = { 22 | type: 'Polygon', 23 | coordinates: [ 24 | [ 25 | [-0.0126953125, -0.072265625], 26 | [-0.1689453125, 0.0107421875], 27 | [0.0458984375, 0.1142578125], 28 | [0.16015625, 0.087890625], 29 | [0.1455078125, 0.020507812500000888], 30 | [0.111328125, -0.0361328125], 31 | [0.0078125, 0.0283203125], 32 | [-0.0126953125, -0.072265625] 33 | ] 34 | ] 35 | }; 36 | 37 | describe('wgs84ToTileCoords', () => { 38 | test('projects correctly', () => { 39 | const transformedGeometry = transformToTileCoords(GEOMETRY_AS_WGS84, TILE_2_3_3_BBOX); 40 | 41 | expect(transformedGeometry).toEqual(GEOMETRY_AS_TILE_COORDS); 42 | }); 43 | 44 | test("raises error if geometry type isn't covered", () => { 45 | expect(() => 46 | transformToTileCoords( 47 | { ...GEOMETRY_AS_WGS84, type: 'MultiMovidas' }, 48 | TILE_2_3_3_BBOX 49 | ) 50 | ).toThrowError(Error); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /packages/react-ui/src/hooks/useImperativeIntl.js: -------------------------------------------------------------------------------- 1 | import { createIntl, createIntlCache } from 'react-intl'; 2 | import { messages } from '../localization'; 3 | import { useMemo } from 'react'; 4 | import { 5 | flattenMessages, 6 | findMatchingMessagesLocale, 7 | DEFAULT_LOCALE 8 | } from '../localization/localeUtils'; 9 | 10 | const cache = createIntlCache(); 11 | const intlInstanceCache = new WeakMap(); 12 | 13 | const createIntlInstance = (intlConfig) => { 14 | const locale = intlConfig?.locale || DEFAULT_LOCALE; 15 | const messagesLocale = findMatchingMessagesLocale(locale, messages); 16 | const intMessages = { 17 | ...(messages[messagesLocale] || {}), 18 | ...(intlConfig?.messages || {}) 19 | }; 20 | 21 | const combinedMessages = flattenMessages(intMessages); 22 | return createIntl( 23 | { 24 | locale, 25 | messages: combinedMessages 26 | }, 27 | cache 28 | ); 29 | }; 30 | 31 | const getGloballyCachedIntl = (intlConfig) => { 32 | // This is very simple cache exploits fact that Intl instance is actually same for most of time 33 | // so we can reuse those maps across several instances of same components 34 | // note, useMemo can't cache accross many that globally and flattenMessages over _app_ and c4r messages is quite costly 35 | // and would be paid for every c4r component mounted. 36 | let cachedInstance = intlInstanceCache.get(intlConfig); 37 | if (cachedInstance) { 38 | return cachedInstance; 39 | } 40 | const newInstance = createIntlInstance(intlConfig); 41 | intlInstanceCache.set(intlConfig, newInstance); 42 | return newInstance; 43 | }; 44 | 45 | export default function useImperativeIntl(intlConfig) { 46 | return getGloballyCachedIntl(intlConfig); 47 | } 48 | --------------------------------------------------------------------------------