import type { WidgetType } from "constants/WidgetConstants"; import { DEFAULT_CENTER } from "constants/WidgetConstants"; import React from "react"; import type { WidgetProps, WidgetState } from "widgets/BaseWidget"; import BaseWidget from "widgets/BaseWidget"; import MapComponent from "../component"; import { EventType } from "constants/AppsmithActionConstants/ActionConstants"; import { ValidationTypes } from "constants/WidgetValidation"; import type { Stylesheet } from "entities/AppTheming"; import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory"; import styled from "styled-components"; import { getResponsiveLayoutConfig } from "utils/layoutPropertiesUtils"; import type { DerivedPropertiesMap } from "utils/WidgetFactory"; import type { MarkerProps } from "../constants"; import { getBorderCSSShorthand } from "constants/DefaultTheme"; const DisabledContainer = styled.div<{ borderRadius: string; boxShadow?: string; }>` background-color: white; height: 100%; text-align: center; display: flex; flex-direction: column; border-radius: ${({ borderRadius }) => borderRadius}; box-shadow: ${({ boxShadow }) => boxShadow} !important; border: ${(props) => getBorderCSSShorthand(props.theme.borders[2])}; h1 { margin-top: 15%; margin-bottom: 10%; color: #7c7c7c; } p { color: #0a0b0e; } `; const DefaultCenter = { ...DEFAULT_CENTER, long: DEFAULT_CENTER.lng }; type Center = { lat: number; long: number; [x: string]: any; }; class MapWidget extends BaseWidget { static defaultProps = {}; static getPropertyPaneContentConfig() { return [ { sectionName: "Data", children: [ { propertyName: "mapCenter", label: "Initial location", helpText: "Default location for the map. Search for a location directly in the field.", isJSConvertible: true, controlType: "LOCATION_SEARCH", dependencies: ["googleMapsApiKey"], isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.OBJECT, params: { allowedKeys: [ { name: "lat", type: ValidationTypes.NUMBER, params: { min: -90, max: 90, default: 0, required: true, }, }, { name: "long", type: ValidationTypes.NUMBER, params: { min: -180, max: 180, default: 0, required: true, }, }, ], }, }, }, { propertyName: "defaultMarkers", label: "Default markers", controlType: "INPUT_TEXT", inputType: "ARRAY", helpText: "Sets the default markers on the map", placeholderText: '[{ "lat": "val1", "long": "val2" }]', isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.ARRAY, params: { children: { type: ValidationTypes.OBJECT, params: { required: true, allowedKeys: [ { name: "lat", type: ValidationTypes.NUMBER, params: { min: -90, max: 90, default: 0, required: true, }, }, { name: "long", type: ValidationTypes.NUMBER, params: { min: -180, max: 180, default: 0, required: true, }, }, { name: "title", type: ValidationTypes.TEXT, }, { name: "color", type: ValidationTypes.TEXT, }, ], }, }, }, }, evaluationSubstitutionType: EvaluationSubstitutionType.SMART_SUBSTITUTE, }, ], }, { sectionName: "General", children: [ { propertyName: "zoomLevel", label: "Zoom Level", controlType: "STEP", helpText: "Changes the default zoom of the map", stepType: "ZOOM_PERCENTAGE", isBindProperty: false, isTriggerProperty: false, }, { propertyName: "isVisible", label: "Visible", helpText: "Controls the visibility of the widget", controlType: "SWITCH", isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.BOOLEAN }, }, { propertyName: "animateLoading", label: "Animate Loading", controlType: "SWITCH", helpText: "Controls the loading of the widget", defaultValue: true, isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.BOOLEAN }, }, { propertyName: "enablePickLocation", label: "Enable pick location", helpText: "Allows a user to pick their location", controlType: "SWITCH", isBindProperty: false, isTriggerProperty: false, }, { propertyName: "isClickedMarkerCentered", label: "Map & Marker centering", helpText: "Controls whether the clicked marker is centered on the map", controlType: "SWITCH", isBindProperty: false, isTriggerProperty: false, }, { propertyName: "allowClustering", label: "Enable clustering", controlType: "SWITCH", helpText: "Allows markers to be clustered", defaultValue: false, isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.BOOLEAN }, }, { propertyName: "enableSearch", label: "Enable search location", helpText: "Allows a user to search for a location", controlType: "SWITCH", isBindProperty: false, isTriggerProperty: false, }, ], }, { sectionName: "Create Marker", children: [ { propertyName: "enableCreateMarker", label: "Create new marker", helpText: "Allows users to mark locations on the map", controlType: "SWITCH", isBindProperty: false, isTriggerProperty: false, }, { propertyName: "onCreateMarker", label: "onCreateMarker", helpText: "When create new marker is enabled, this event triggers upon successful marker creation", controlType: "ACTION_SELECTOR", isJSConvertible: true, isBindProperty: true, isTriggerProperty: true, hidden: (props: MapWidgetProps) => { return !props.enableCreateMarker; }, dependencies: ["enableCreateMarker"], }, ], }, ...getResponsiveLayoutConfig(this.getWidgetType()), { sectionName: "Events", children: [ { propertyName: "onMarkerClick", label: "onMarkerClick", helpText: "Triggers an action when the user clicks on the marker", controlType: "ACTION_SELECTOR", isJSConvertible: true, isBindProperty: true, isTriggerProperty: true, }, ], }, ]; } static getPropertyPaneStyleConfig() { return [ { sectionName: "Border and Shadow", children: [ { propertyName: "borderRadius", label: "Border Radius", helpText: "Rounds the corners of the icon button's outer border edge", controlType: "BORDER_RADIUS_OPTIONS", isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, }, { propertyName: "boxShadow", label: "Box Shadow", helpText: "Enables you to cast a drop shadow from the frame of the widget", controlType: "BOX_SHADOW_OPTIONS", isJSConvertible: true, isBindProperty: true, isTriggerProperty: false, validation: { type: ValidationTypes.TEXT }, }, ], }, ]; } static getDefaultPropertiesMap(): Record { return { markers: "defaultMarkers", center: "mapCenter", }; } static getMetaPropertiesMap(): Record { return { center: undefined, markers: undefined, selectedMarker: undefined, }; } static getDerivedPropertiesMap(): DerivedPropertiesMap { return {}; } static getStylesheetConfig(): Stylesheet { return { borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}", boxShadow: "{{appsmith.theme.boxShadow.appBoxShadow}}", }; } updateCenter = (lat?: number, long?: number, title?: string) => { this.props.updateWidgetMetaProperty("center", { lat, long, title }); }; updateMarker = (lat: number, long: number, index: number) => { const markers: Array = [...(this.props.markers || [])].map( (marker, i) => { if (index === i) { marker = { lat, long }; } return marker; }, ); this.disableDrag(false); this.props.updateWidgetMetaProperty("markers", markers); }; onCreateMarker = (lat?: number, long?: number) => { this.disableDrag(true); const marker = { lat, long, title: "" }; const markers = []; (this.props.markers || []).forEach((m) => { markers.push(m); }); markers.push(marker); this.props.updateWidgetMetaProperty("markers", markers); this.props.updateWidgetMetaProperty("selectedMarker", marker, { triggerPropertyName: "onCreateMarker", dynamicString: this.props.onCreateMarker, event: { type: EventType.ON_CREATE_MARKER, }, }); }; unselectMarker = () => { this.props.updateWidgetMetaProperty("selectedMarker", undefined); }; onMarkerClick = (lat?: number, long?: number, title?: string) => { this.disableDrag(true); const selectedMarker = { lat: lat, long: long, title: title, }; this.props.updateWidgetMetaProperty("selectedMarker", selectedMarker, { triggerPropertyName: "onMarkerClick", dynamicString: this.props.onMarkerClick, event: { type: EventType.ON_MARKER_CLICK, }, }); }; getCenter(): Center { return this.props.center || this.props.mapCenter || DefaultCenter; } componentDidUpdate(prevProps: MapWidgetProps) { //remove selectedMarker when map initial location is updated if ( JSON.stringify(prevProps.center) !== JSON.stringify(this.props.center) && this.props.selectedMarker ) { this.unselectMarker(); } // If initial location was changed if ( JSON.stringify(prevProps.mapCenter) !== JSON.stringify(this.props.mapCenter) ) { this.props.updateWidgetMetaProperty("center", this.props.mapCenter); return; } // If markers were changed if ( this.props.markers && this.props.markers.length > 0 && JSON.stringify(prevProps.markers) !== JSON.stringify(this.props.markers) ) { this.props.updateWidgetMetaProperty( "center", this.props.markers[this.props.markers.length - 1], ); } } enableDrag = () => { this.disableDrag(false); }; getPageView() { return ( <> {!this.props.googleMapsApiKey && (

{"Map Widget disabled"}

Key: x{this.props.googleMapsApiKey}x

{"Map widget requires a Google Maps API Key"}

{"See our"} {" documentation "} {"to configure API Keys"}

)} {this.props.googleMapsApiKey && ( )} ); } static getWidgetType(): WidgetType { return "MAP_WIDGET"; } } export interface MapWidgetProps extends WidgetProps { googleMapsApiKey?: string; isDisabled?: boolean; isVisible?: boolean; enableSearch: boolean; zoomLevel: number; allowZoom: boolean; enablePickLocation: boolean; mapCenter: { lat: number; long: number; title?: string; }; center?: { lat: number; long: number; }; defaultMarkers?: Array; markers?: Array; selectedMarker?: { lat: number; long: number; title?: string; color?: string; }; onMarkerClick?: string; onCreateMarker?: string; borderRadius: string; boxShadow?: string; allowClustering?: boolean; } export default MapWidget;