// Copyright (c) 2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import React, {Component} from 'react'; import {css} from 'styled-components'; import {createSelector} from 'reselect'; import get from 'lodash/get'; import document from 'global/document'; import {EXPORT_DATA_TYPE_OPTIONS, EXPORT_MAP_FORMATS} from 'constants/default-settings'; import ModalDialogFactory from './modals/modal-dialog'; import {exportJson, exportHtml, exportData, exportImage, exportMap} from 'utils/export-utils'; import {isValidMapInfo} from 'utils/map-info-utils'; // modals import DeleteDatasetModalFactory from './modals/delete-data-modal'; import OverWriteMapModalFactory from './modals/overwrite-map-modal'; import DataTableModalFactory from './modals/data-table-modal'; import LoadDataModalFactory from './modals/load-data-modal'; import ExportImageModalFactory from './modals/export-image-modal'; import ExportDataModalFactory from './modals/export-data-modal'; import ExportMapModalFactory from './modals/export-map-modal/export-map-modal'; import AddMapStyleModalFactory from './modals/add-map-style-modal'; import SaveMapModalFactory from './modals/save-map-modal'; import ShareMapModalFactory from './modals/share-map-modal'; // Breakpoints import {media} from 'styles/media-breakpoints'; // Template import { ADD_DATA_ID, DATA_TABLE_ID, DELETE_DATA_ID, EXPORT_DATA_ID, EXPORT_IMAGE_ID, EXPORT_MAP_ID, ADD_MAP_STYLE_ID, SAVE_MAP_ID, SHARE_MAP_ID, OVERWRITE_MAP_ID } from 'constants/default-settings'; import KeyEvent from 'constants/keyevent'; import {getFileFormatNames, getFileExtensions} from '../reducers/vis-state-selectors'; import {MapState, MapStyle, UiState, VisState} from 'reducers'; import {OnSuccessCallBack, OnErrorCallBack} from 'actions'; import {ProviderState} from 'reducers/provider-state-updaters'; import * as VisStateActions from 'actions/vis-state-actions'; import * as UIStateActions from 'actions/ui-state-actions'; import * as MapStyleActions from 'actions/map-style-actions'; import * as ProviderActions from 'actions/provider-actions'; import {ModalDialogProps} from './common/modal'; import {Provider} from 'cloud-providers'; const DataTableModalStyle = css` top: 80px; padding: 32px 0 0 0; width: 90vw; max-width: 90vw; ${media.portable` padding: 0; `} ${media.palm` padding: 0; margin: 0 auto; `}; `; const smallModalCss = css` width: 40%; padding: 40px 40px 32px 40px; `; const LoadDataModalStyle = css` top: 60px; `; const DefaultStyle = css` max-width: 960px; `; export type ModalContainerProps = { appName: string; rootNode: React.ReactInstance | null | undefined; containerW: number; containerH: number; mapboxApiUrl?: string; mapState: MapState; mapStyle: MapStyle; uiState: UiState; visState: VisState; providerState: ProviderState; visStateActions: typeof VisStateActions; uiStateActions: typeof UIStateActions; mapStyleActions: typeof MapStyleActions; providerActions: typeof ProviderActions; onSaveToStorage?: () => void; cloudProviders: Provider[]; onLoadCloudMapSuccess?: OnSuccessCallBack; onLoadCloudMapError?: OnErrorCallBack; onExportToCloudSuccess?: OnSuccessCallBack; onExportToCloudError?: OnErrorCallBack; }; ModalContainerFactory.deps = [ DeleteDatasetModalFactory, OverWriteMapModalFactory, DataTableModalFactory, LoadDataModalFactory, ExportImageModalFactory, ExportDataModalFactory, ExportMapModalFactory, AddMapStyleModalFactory, ModalDialogFactory, SaveMapModalFactory, ShareMapModalFactory ]; export default function ModalContainerFactory( DeleteDatasetModal: ReturnType, OverWriteMapModal: ReturnType, DataTableModal: ReturnType, LoadDataModal: ReturnType, ExportImageModal: ReturnType, ExportDataModal: ReturnType, ExportMapModal: ReturnType, AddMapStyleModal: ReturnType, ModalDialog: ReturnType, SaveMapModal: ReturnType, ShareMapModal: ReturnType ): React.ElementType { /** @augments React.Component */ class ModalContainer extends Component { // TODO - remove when prop types are fully exported componentDidMount = () => { document.addEventListener('keyup', this._onKeyUp); }; componentWillUnmount() { document.removeEventListener('keyup', this._onKeyUp); } cloudProviders = props => props.cloudProviders; providerWithStorage = createSelector(this.cloudProviders, cloudProviders => cloudProviders.filter(p => p.hasPrivateStorage()) ); providerWithShare = createSelector(this.cloudProviders, cloudProviders => cloudProviders.filter(p => p.hasSharingUrl()) ); _onKeyUp = event => { const keyCode = event.keyCode; if (keyCode === KeyEvent.DOM_VK_ESCAPE) { this._closeModal(); } }; _closeModal = () => { this.props.uiStateActions.toggleModal(null); }; _deleteDataset = key => { this.props.visStateActions.removeDataset(key); this._closeModal(); }; _onAddCustomMapStyle = () => { this.props.mapStyleActions.addCustomMapStyle(); this._closeModal(); }; _onFileUpload = fileList => { this.props.visStateActions.loadFiles(fileList); }; _onExportImage = () => { if (!this.props.uiState.exportImage.processing) { // @ts-ignore TODO: fix exportImage method exportImage(this.props.uiState.exportImage, `${this.props.appName}.png`); this.props.uiStateActions.cleanupExportImage(); this._closeModal(); } }; _onExportData = () => { exportData(this.props, this.props.uiState.exportData); this._closeModal(); }; _onExportMap = () => { const {uiState} = this.props; const {format} = uiState.exportMap; (format === EXPORT_MAP_FORMATS.HTML ? exportHtml : exportJson)( this.props, this.props.uiState.exportMap[format] || {} ); this._closeModal(); }; _exportFileToCloud = ({provider, isPublic, overwrite, closeModal}) => { const toSave = exportMap(this.props); this.props.providerActions.exportFileToCloud({ // @ts-ignore mapData: toSave, provider, options: { isPublic, overwrite }, closeModal, onSuccess: this.props.onExportToCloudSuccess, onError: this.props.onExportToCloudError }); }; _onSaveMap = (overwrite = false) => { const {currentProvider} = this.props.providerState; // @ts-ignore const provider = this.props.cloudProviders.find(p => p.name === currentProvider); this._exportFileToCloud({ provider, isPublic: false, overwrite, closeModal: true }); }; _onOverwriteMap = () => { this._onSaveMap(true); }; _onShareMapUrl = provider => { this._exportFileToCloud({provider, isPublic: true, overwrite: false, closeModal: false}); }; _onCloseSaveMap = () => { this.props.providerActions.resetProviderStatus(); this._closeModal(); }; _onLoadCloudMap = payload => { this.props.providerActions.loadCloudMap({ ...payload, onSuccess: this.props.onLoadCloudMapSuccess, onError: this.props.onLoadCloudMapError }); }; /* eslint-disable complexity */ render() { const { containerW, containerH, mapStyle, mapState, uiState, visState, visStateActions, uiStateActions, providerState } = this.props; const {currentModal, datasetKeyToRemove} = uiState; const {datasets, layers, editingDataset} = visState; let template: JSX.Element | null = null; let modalProps: Partial = {}; // TODO - currentModal is a string // @ts-ignore if (currentModal && currentModal.id && currentModal.template) { // if currentMdoal template is already provided // TODO: need to check whether template is valid // @ts-ignore template = ; // @ts-ignore modalProps = currentModal.modalProps; } else { switch (currentModal) { case DATA_TABLE_ID: const width = containerW * 0.9; template = ( ); // TODO: we need to make this width consistent with the css rule defined modal.js:32 max-width: 70vw // @ts-ignore // TODO fix this after add types to Theme modalProps.cssStyle = css` ${DataTableModalStyle}; ${media.palm` width: ${width}px; `}; `; break; case DELETE_DATA_ID: // validate options if (datasetKeyToRemove && datasets && datasets[datasetKeyToRemove]) { template = ( ); modalProps = { title: 'modal.title.deleteDataset', cssStyle: smallModalCss, footer: true, onConfirm: () => this._deleteDataset(datasetKeyToRemove), onCancel: this._closeModal, confirmButton: { negative: true, large: true, children: 'modal.button.delete' } }; } break; // in case we add a new case after this one case ADD_DATA_ID: template = ( ); modalProps = { title: 'modal.title.addDataToMap', cssStyle: LoadDataModalStyle, footer: false, onConfirm: this._closeModal }; break; case EXPORT_IMAGE_ID: template = ( ); modalProps = { title: 'modal.title.exportImage', cssStyle: '', footer: true, onCancel: this._closeModal, onConfirm: this._onExportImage, confirmButton: { large: true, disabled: uiState.exportImage.processing, children: 'modal.button.download' } }; break; case EXPORT_DATA_ID: template = ( ); modalProps = { title: 'modal.title.exportData', cssStyle: '', footer: true, onCancel: this._closeModal, onConfirm: this._onExportData, confirmButton: { large: true, children: 'modal.button.export' } }; break; case EXPORT_MAP_ID: const keplerGlConfig = visState.schema.getConfigToSave({ mapStyle, visState, mapState, uiState }); template = ( ); modalProps = { title: 'modal.title.exportMap', cssStyle: '', footer: true, onCancel: this._closeModal, onConfirm: this._onExportMap, confirmButton: { large: true, children: 'modal.button.export' } }; break; case ADD_MAP_STYLE_ID: template = ( ); modalProps = { title: 'modal.title.addCustomMapboxStyle', cssStyle: '', footer: true, onCancel: this._closeModal, onConfirm: this._onAddCustomMapStyle, confirmButton: { large: true, disabled: !mapStyle.inputStyle.style, children: 'modal.button.addStyle' } }; break; case SAVE_MAP_ID: template = ( ); modalProps = { title: 'modal.title.saveMap', cssStyle: '', footer: true, onCancel: this._closeModal, onConfirm: () => this._onSaveMap(false), confirmButton: { large: true, disabled: uiState.exportImage.processing || !isValidMapInfo(visState.mapInfo) || !providerState.currentProvider, children: 'modal.button.save' } }; break; case OVERWRITE_MAP_ID: template = ( ); modalProps = { title: 'Overwrite Existing File?', cssStyle: smallModalCss, footer: true, onConfirm: this._onOverwriteMap, onCancel: this._closeModal, confirmButton: { large: true, children: 'Yes', disabled: uiState.exportImage.processing || !isValidMapInfo(visState.mapInfo) || !providerState.currentProvider } }; break; case SHARE_MAP_ID: template = ( ); modalProps = { title: 'modal.title.shareURL', cssStyle: '', onCancel: this._onCloseSaveMap }; break; default: break; } } return this.props.rootNode ? ( {template} ) : null; } /* eslint-enable complexity */ } return ModalContainer; }