diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000..5c3484557d --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,4 @@ +# .git-blame-ignore-revs + +# Applied automated eslint autofixes and prettier to map.es6 +be3a9b6041e451b02c671220be50b5ca33cae1a8 diff --git a/jsapp/js/account/accountSettingsRoute.tsx b/jsapp/js/account/accountSettingsRoute.tsx index a5835f6d2e..2b5e301eb8 100644 --- a/jsapp/js/account/accountSettingsRoute.tsx +++ b/jsapp/js/account/accountSettingsRoute.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useState} from 'react'; import {observer} from 'mobx-react'; +import {unstable_usePrompt as usePrompt} from 'react-router-dom'; import bem, {makeBem} from 'js/bem'; -import {usePrompt} from 'js/router/promptBlocker'; import sessionStore from 'js/stores/session'; import './accountSettings.scss'; import Checkbox from '../components/common/checkbox'; @@ -134,10 +134,10 @@ const AccountSettings = observer(() => { }); } }, [sessionStore.isPending]); - usePrompt( - t('You have unsaved changes. Leave settings without saving?'), - !form.isPristine - ); + usePrompt({ + when: !form.isPristine, + message: t('You have unsaved changes. Leave settings without saving?'), + }); const updateProfile = () => { // To patch correctly with recent changes to the backend, // ensure that we send empty strings if the field is left blank. diff --git a/jsapp/js/app.js b/jsapp/js/app.js index 4bb86dfef5..a702a72041 100644 --- a/jsapp/js/app.js +++ b/jsapp/js/app.js @@ -5,7 +5,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import DocumentTitle from 'react-document-title'; -import { Outlet } from "react-router-dom"; +import {Outlet} from 'react-router-dom'; import reactMixin from 'react-mixin'; import Reflux from 'reflux'; import {stores} from 'js/stores'; @@ -20,9 +20,8 @@ import PermValidator from 'js/components/permissions/permValidator'; import {assign} from 'utils'; import BigModal from 'js/components/bigModal/bigModal'; import {Toaster} from 'react-hot-toast'; -import { withRouter, routerGetAssetId } from './router/legacy'; -import { history } from "./router/historyRouter"; - +import {withRouter, routerGetAssetId, router} from './router/legacy'; +import {Tracking} from './router/useTracking'; class App extends React.Component { constructor(props) { @@ -33,7 +32,7 @@ class App extends React.Component { } componentDidMount() { - history.listen(this.onRouteChange.bind(this)); + router.subscribe(this.onRouteChange.bind(this)); } onRouteChange() { @@ -66,33 +65,42 @@ class App extends React.Component { }; if (typeof this.state.pageState.modal === 'object') { - pageWrapperModifiers[`is-modal-${this.state.pageState.modal.type}`] = true; + pageWrapperModifiers[ + `is-modal-${this.state.pageState.modal.type}` + ] = true; } return ( - -
- - { this.state.pageState.modal && + + +
+ + {this.state.pageState.modal && ( - } + )} - { !this.isFormBuilder() && + {!this.isFormBuilder() && ( - - + + - } + )} - - { !this.isFormBuilder() && + + {!this.isFormBuilder() && ( - } + )} diff --git a/jsapp/js/components/drawer.es6 b/jsapp/js/components/drawer.es6 index bcd5c22ee0..4742a04d53 100644 --- a/jsapp/js/components/drawer.es6 +++ b/jsapp/js/components/drawer.es6 @@ -20,8 +20,7 @@ import {ROUTES} from 'js/router/routerConstants'; import {assign} from 'utils'; import SidebarFormsList from '../lists/sidebarForms'; import envStore from 'js/envStore'; -import {history} from 'js/router/historyRouter'; -import { routerIsActive, withRouter } from '../router/legacy'; +import { router, routerIsActive, withRouter } from '../router/legacy'; const AccountSidebar = lazy(() => import("js/account/accountSidebar")); @@ -51,9 +50,7 @@ const FormSidebar = observer(class FormSidebar extends Reflux.Component { autoBind(this); } componentDidMount() { - this.unlisteners.push( - history.listen(this.onRouteChange.bind(this)) - ); + router.subscribe(this.onRouteChange.bind(this)); } componentWillUnmount() { this.unlisteners.forEach((clb) => {clb();}); diff --git a/jsapp/js/components/formEditors.es6 b/jsapp/js/components/formEditors.js similarity index 100% rename from jsapp/js/components/formEditors.es6 rename to jsapp/js/components/formEditors.js diff --git a/jsapp/js/components/formJson.es6 b/jsapp/js/components/formJson.js similarity index 100% rename from jsapp/js/components/formJson.es6 rename to jsapp/js/components/formJson.js diff --git a/jsapp/js/components/formLanding.es6 b/jsapp/js/components/formLanding.js similarity index 100% rename from jsapp/js/components/formLanding.es6 rename to jsapp/js/components/formLanding.js diff --git a/jsapp/js/components/formNotFound.es6 b/jsapp/js/components/formNotFound.js similarity index 100% rename from jsapp/js/components/formNotFound.es6 rename to jsapp/js/components/formNotFound.js diff --git a/jsapp/js/components/formSubScreens.es6 b/jsapp/js/components/formSubScreens.js similarity index 97% rename from jsapp/js/components/formSubScreens.es6 rename to jsapp/js/components/formSubScreens.js index 8d4ffa721a..15bdc4bfe3 100644 --- a/jsapp/js/components/formSubScreens.es6 +++ b/jsapp/js/components/formSubScreens.js @@ -111,9 +111,6 @@ export class FormSubScreens extends React.Component { hookUid={this.props.params.hookUid} /> ); - case ROUTES.FORM_KOBOCAT.replace(':uid', this.state.uid): - iframeUrl = deployment__identifier + '/form_settings'; - break; case ROUTES.FORM_RESET.replace(':uid', this.state.uid): return this.renderReset(); } diff --git a/jsapp/js/components/formSummary.es6 b/jsapp/js/components/formSummary.js similarity index 100% rename from jsapp/js/components/formSummary.es6 rename to jsapp/js/components/formSummary.js diff --git a/jsapp/js/components/formViewTabs.es6 b/jsapp/js/components/formViewTabs.es6 index 510c3433da..2be90fbe66 100644 --- a/jsapp/js/components/formViewTabs.es6 +++ b/jsapp/js/components/formViewTabs.es6 @@ -64,6 +64,7 @@ class FormViewTabs extends Reflux.Component { triggerRefresh(evt) { if ($(evt.target).hasClass('active')) { + // ROUTES.FORM_RESET this.props.router.navigate(`/forms/${this.state.asset.uid}/reset`); var path = evt.target.getAttribute('data-path'); diff --git a/jsapp/js/components/formXform.es6 b/jsapp/js/components/formXform.js similarity index 100% rename from jsapp/js/components/formXform.es6 rename to jsapp/js/components/formXform.js diff --git a/jsapp/js/components/header/searchBoxStore.ts b/jsapp/js/components/header/searchBoxStore.ts index e30c67e07a..8fb66f2143 100644 --- a/jsapp/js/components/header/searchBoxStore.ts +++ b/jsapp/js/components/header/searchBoxStore.ts @@ -1,17 +1,18 @@ import Reflux from 'reflux'; +import type {RouterState} from '@remix-run/router'; import { getCurrentPath, isMyLibraryRoute, isPublicCollectionsRoute, } from 'js/router/routerUtils'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; const DEFAULT_SEARCH_PHRASE = ''; type SearchBoxContextName = 'MY_LIBRARY' | 'PUBLIC_COLLECTIONS'; export const SEARCH_CONTEXTS: { - [name in SearchBoxContextName]: SearchBoxContextName + [name in SearchBoxContextName]: SearchBoxContextName; } = { MY_LIBRARY: 'MY_LIBRARY', PUBLIC_COLLECTIONS: 'PUBLIC_COLLECTIONS', @@ -30,13 +31,15 @@ class SearchBoxStore extends Reflux.Store { }; init() { - history.listen(this.onRouteChange.bind(this)); + setTimeout(() => router!.subscribe(this.onRouteChange.bind(this))); this.resetContext(); } // manages clearing search when switching main routes - onRouteChange(data: any) { - if (this.previousPath.split('/')[1] !== data.location.pathname.split('/')[1]) { + onRouteChange(data: RouterState) { + if ( + this.previousPath.split('/')[1] !== data.location.pathname.split('/')[1] + ) { this.clear(); } this.previousPath = data.location.pathname; diff --git a/jsapp/js/components/languages/languageSelector.scss b/jsapp/js/components/languages/languageSelector.scss index 8a2bd75a49..f174d9702f 100644 --- a/jsapp/js/components/languages/languageSelector.scss +++ b/jsapp/js/components/languages/languageSelector.scss @@ -101,6 +101,7 @@ padding: 0 sizes.$x40; margin: 0; background: transparent; + line-height: sizes.$x40; } .language-selector__selected-language-label { @@ -115,6 +116,9 @@ opacity: 1; color: colors.$kobo-gray-55; } + &:focus { + outline: none; + } } .language-selector__clear-selected-language, diff --git a/jsapp/js/components/library/assetRoute.es6 b/jsapp/js/components/library/assetRoute.js similarity index 100% rename from jsapp/js/components/library/assetRoute.es6 rename to jsapp/js/components/library/assetRoute.js diff --git a/jsapp/js/components/library/myLibraryRoute.es6 b/jsapp/js/components/library/myLibraryRoute.js similarity index 100% rename from jsapp/js/components/library/myLibraryRoute.es6 rename to jsapp/js/components/library/myLibraryRoute.js diff --git a/jsapp/js/components/library/myLibraryStore.ts b/jsapp/js/components/library/myLibraryStore.ts index a465db7b30..b21d4f7dc2 100644 --- a/jsapp/js/components/library/myLibraryStore.ts +++ b/jsapp/js/components/library/myLibraryStore.ts @@ -1,20 +1,17 @@ import debounce from 'lodash.debounce'; import Reflux from 'reflux'; -import type {Location} from 'history'; import searchBoxStore from '../header/searchBoxStore'; import assetUtils from 'js/assetUtils'; -import { - getCurrentPath, - isAnyLibraryRoute, -} from 'js/router/routerUtils'; +import {getCurrentPath, isAnyLibraryRoute} from 'js/router/routerUtils'; import {actions} from 'js/actions'; import { ORDER_DIRECTIONS, ASSETS_TABLE_COLUMNS, } from 'js/components/assetsTable/assetsTableConstants'; import type {OrderDirection} from 'js/projects/projectViews/constants'; +import type {RouterState} from '@remix-run/router'; import {ROUTES} from 'js/router/routerConstants'; -import { history } from "js/router/historyRouter"; +import {router} from 'js/router/legacy'; import type { AssetResponse, AssetsResponse, @@ -76,26 +73,54 @@ class MyLibraryStore extends Reflux.Store { this.setDefaultColumns(); - history.listen(this.onRouteChange.bind(this)); + setTimeout(() => router!.subscribe(this.onRouteChange.bind(this))); searchBoxStore.listen(this.searchBoxStoreChanged, this); - actions.library.moveToCollection.completed.listen(this.onMoveToCollectionCompleted.bind(this)); - actions.library.subscribeToCollection.completed.listen(this.fetchData.bind(this, true)); - actions.library.unsubscribeFromCollection.completed.listen(this.fetchData.bind(this, true)); - actions.library.searchMyLibraryAssets.started.listen(this.onSearchStarted.bind(this)); - actions.library.searchMyLibraryAssets.completed.listen(this.onSearchCompleted.bind(this)); - actions.library.searchMyLibraryAssets.failed.listen(this.onSearchFailed.bind(this)); - actions.library.searchMyLibraryMetadata.completed.listen(this.onSearchMetadataCompleted.bind(this)); - actions.resources.loadAsset.completed.listen(this.onAssetChanged.bind(this)); - actions.resources.updateAsset.completed.listen(this.onAssetChanged.bind(this)); - actions.resources.cloneAsset.completed.listen(this.onAssetCreated.bind(this)); - actions.resources.createResource.completed.listen(this.onAssetCreated.bind(this)); - actions.resources.deleteAsset.completed.listen(this.onDeleteAssetCompleted.bind(this)); + actions.library.moveToCollection.completed.listen( + this.onMoveToCollectionCompleted.bind(this) + ); + actions.library.subscribeToCollection.completed.listen( + this.fetchData.bind(this, true) + ); + actions.library.unsubscribeFromCollection.completed.listen( + this.fetchData.bind(this, true) + ); + actions.library.searchMyLibraryAssets.started.listen( + this.onSearchStarted.bind(this) + ); + actions.library.searchMyLibraryAssets.completed.listen( + this.onSearchCompleted.bind(this) + ); + actions.library.searchMyLibraryAssets.failed.listen( + this.onSearchFailed.bind(this) + ); + actions.library.searchMyLibraryMetadata.completed.listen( + this.onSearchMetadataCompleted.bind(this) + ); + actions.resources.loadAsset.completed.listen( + this.onAssetChanged.bind(this) + ); + actions.resources.updateAsset.completed.listen( + this.onAssetChanged.bind(this) + ); + actions.resources.cloneAsset.completed.listen( + this.onAssetCreated.bind(this) + ); + actions.resources.createResource.completed.listen( + this.onAssetCreated.bind(this) + ); + actions.resources.deleteAsset.completed.listen( + this.onDeleteAssetCompleted.bind(this) + ); // TODO Improve reaction to uploads being finished after task is done: // https://github.com/kobotoolbox/kpi/issues/476 - actions.resources.createImport.completed.listen(this.fetchDataDebounced.bind(this, true)); + actions.resources.createImport.completed.listen( + this.fetchDataDebounced.bind(this, true) + ); // startup store after config is ready - actions.permissions.getConfig.completed.listen(this.startupStore.bind(this)); + actions.permissions.getConfig.completed.listen( + this.startupStore.bind(this) + ); } /** @@ -103,7 +128,11 @@ class MyLibraryStore extends Reflux.Store { * otherwise wait until route changes to a library (see `onRouteChange`) */ startupStore() { - if (!this.isInitialised && isAnyLibraryRoute() && !this.data.isFetchingData) { + if ( + !this.isInitialised && + isAnyLibraryRoute() && + !this.data.isFetchingData + ) { this.fetchData(true); } } @@ -154,23 +183,26 @@ class MyLibraryStore extends Reflux.Store { params.metadata = needsMetadata; const orderColumn = ASSETS_TABLE_COLUMNS[this.data.orderColumnId]; - const direction = this.data.orderValue === ORDER_DIRECTIONS.ascending ? '' : '-'; + const direction = + this.data.orderValue === ORDER_DIRECTIONS.ascending ? '' : '-'; params.ordering = `${direction}${orderColumn.orderBy}`; actions.library.searchMyLibraryAssets(params); } - onRouteChange(data: any) { - if (!this.isInitialised && isAnyLibraryRoute() && !this.data.isFetchingData) { + onRouteChange(data: RouterState) { + if ( + !this.isInitialised && + isAnyLibraryRoute() && + !this.data.isFetchingData + ) { this.fetchData(true); } else if ( - ( - // coming from outside of library - this.previousPath.split('/')[1] !== 'library' || + // coming from outside of library + (this.previousPath.split('/')[1] !== 'library' || // public-collections is a special case that is kinda in library, but // actually outside of it - this.previousPath.startsWith(ROUTES.PUBLIC_COLLECTIONS) - ) && + this.previousPath.startsWith(ROUTES.PUBLIC_COLLECTIONS)) && isAnyLibraryRoute() ) { // refresh data when navigating into library from other place @@ -210,7 +242,10 @@ class MyLibraryStore extends Reflux.Store { } this.data.totalSearchAssets = response.count; // update total count for the first time and the ones that will get a full count - if (this.data.totalUserAssets === null || searchBoxStore.getSearchPhrase() === '') { + if ( + this.data.totalUserAssets === null || + searchBoxStore.getSearchPhrase() === '' + ) { this.data.totalUserAssets = this.data.totalSearchAssets; } this.data.isFetchingData = false; @@ -253,12 +288,10 @@ class MyLibraryStore extends Reflux.Store { const loopAsset = this.data.assets[i]; if ( loopAsset.uid === asset.uid && - ( - // if the changed asset didn't change (e.g. was just loaded) - // let's not cause it to fetchMetadata - loopAsset.date_modified !== asset.date_modified || - loopAsset.version_id !== asset.version_id - ) + // if the changed asset didn't change (e.g. was just loaded) + // let's not cause it to fetchMetadata + (loopAsset.date_modified !== asset.date_modified || + loopAsset.version_id !== asset.version_id) ) { this.data.assets[i] = asset; wasUpdated = true; @@ -273,10 +306,7 @@ class MyLibraryStore extends Reflux.Store { } onAssetCreated(asset: AssetResponse) { - if ( - assetUtils.isLibraryAsset(asset.asset_type) && - asset.parent === null - ) { + if (assetUtils.isLibraryAsset(asset.asset_type) && asset.parent === null) { if (this.data.totalUserAssets !== null) { this.data.totalUserAssets++; } diff --git a/jsapp/js/components/library/ownedCollectionsStore.ts b/jsapp/js/components/library/ownedCollectionsStore.ts index 8e2087c265..7b0b911693 100644 --- a/jsapp/js/components/library/ownedCollectionsStore.ts +++ b/jsapp/js/components/library/ownedCollectionsStore.ts @@ -10,7 +10,7 @@ import type { AssetsResponse, DeleteAssetResponse, } from 'js/dataInterface'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; export interface OwnedCollectionsStoreData { isFetchingData: boolean; @@ -38,7 +38,7 @@ class OwnedCollectionsStore extends Reflux.Store { actions.resources.deleteAsset.completed.listen(this.onDeleteAssetCompleted.bind(this)); when(() => sessionStore.isLoggedIn, this.startupStore.bind(this)); - history.listen(this.startupStore.bind(this)); + setTimeout(() => router!.subscribe(this.startupStore.bind(this))); this.startupStore(); } diff --git a/jsapp/js/components/library/publicCollectionsRoute.es6 b/jsapp/js/components/library/publicCollectionsRoute.js similarity index 100% rename from jsapp/js/components/library/publicCollectionsRoute.es6 rename to jsapp/js/components/library/publicCollectionsRoute.js diff --git a/jsapp/js/components/library/publicCollectionsStore.ts b/jsapp/js/components/library/publicCollectionsStore.ts index 41866f1151..7c13ae8a6e 100644 --- a/jsapp/js/components/library/publicCollectionsStore.ts +++ b/jsapp/js/components/library/publicCollectionsStore.ts @@ -1,23 +1,19 @@ import Reflux from 'reflux'; -import type {Update} from 'history'; -import searchBoxStore, {SEARCH_CONTEXTS} from 'js/components/header/searchBoxStore'; +import type {RouterState} from '@remix-run/router'; +import searchBoxStore, { + SEARCH_CONTEXTS, +} from 'js/components/header/searchBoxStore'; import assetUtils from 'js/assetUtils'; -import { - getCurrentPath, - isPublicCollectionsRoute, -} from 'js/router/routerUtils'; +import {getCurrentPath, isPublicCollectionsRoute} from 'js/router/routerUtils'; import {actions} from 'js/actions'; import { ORDER_DIRECTIONS, ASSETS_TABLE_COLUMNS, } from 'js/components/assetsTable/assetsTableConstants'; import type {AssetsTableColumn} from 'js/components/assetsTable/assetsTableConstants'; -import { - ASSET_TYPES, - ACCESS_TYPES, -} from 'js/constants'; +import {ASSET_TYPES, ACCESS_TYPES} from 'js/constants'; import {ROUTES} from 'js/router/routerConstants'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; import type { AssetResponse, AssetsResponse, @@ -80,23 +76,49 @@ class PublicCollectionsStore extends Reflux.Store { init() { this.setDefaultColumns(); - history.listen(this.onRouteChange.bind(this)); + setTimeout(() => router!.subscribe(this.onRouteChange.bind(this))); searchBoxStore.listen(this.searchBoxStoreChanged.bind(this), this); - actions.library.searchPublicCollections.started.listen(this.onSearchStarted.bind(this)); - actions.library.searchPublicCollections.completed.listen(this.onSearchCompleted.bind(this)); - actions.library.searchPublicCollections.failed.listen(this.onSearchFailed.bind(this)); - actions.library.searchPublicCollectionsMetadata.completed.listen(this.onSearchMetadataCompleted.bind(this)); - actions.library.subscribeToCollection.completed.listen(this.onSubscribeCompleted.bind(this)); - actions.library.unsubscribeFromCollection.listen(this.onUnsubscribeCompleted.bind(this)); - actions.library.moveToCollection.completed.listen(this.onMoveToCollectionCompleted.bind(this)); - actions.resources.loadAsset.completed.listen(this.onAssetChanged.bind(this)); - actions.resources.updateAsset.completed.listen(this.onAssetChanged.bind(this)); - actions.resources.cloneAsset.completed.listen(this.onAssetCreated.bind(this)); - actions.resources.createResource.completed.listen(this.onAssetCreated.bind(this)); - actions.resources.deleteAsset.completed.listen(this.onDeleteAssetCompleted.bind(this)); + actions.library.searchPublicCollections.started.listen( + this.onSearchStarted.bind(this) + ); + actions.library.searchPublicCollections.completed.listen( + this.onSearchCompleted.bind(this) + ); + actions.library.searchPublicCollections.failed.listen( + this.onSearchFailed.bind(this) + ); + actions.library.searchPublicCollectionsMetadata.completed.listen( + this.onSearchMetadataCompleted.bind(this) + ); + actions.library.subscribeToCollection.completed.listen( + this.onSubscribeCompleted.bind(this) + ); + actions.library.unsubscribeFromCollection.listen( + this.onUnsubscribeCompleted.bind(this) + ); + actions.library.moveToCollection.completed.listen( + this.onMoveToCollectionCompleted.bind(this) + ); + actions.resources.loadAsset.completed.listen( + this.onAssetChanged.bind(this) + ); + actions.resources.updateAsset.completed.listen( + this.onAssetChanged.bind(this) + ); + actions.resources.cloneAsset.completed.listen( + this.onAssetCreated.bind(this) + ); + actions.resources.createResource.completed.listen( + this.onAssetCreated.bind(this) + ); + actions.resources.deleteAsset.completed.listen( + this.onDeleteAssetCompleted.bind(this) + ); // startup store after config is ready - actions.permissions.getConfig.completed.listen(this.startupStore.bind(this)); + actions.permissions.getConfig.completed.listen( + this.startupStore.bind(this) + ); } /** @@ -104,7 +126,11 @@ class PublicCollectionsStore extends Reflux.Store { * otherwise wait until route changes to a library (see `onRouteChange`) */ startupStore() { - if (!this.isInitialised && isPublicCollectionsRoute() && !this.data.isFetchingData) { + if ( + !this.isInitialised && + isPublicCollectionsRoute() && + !this.data.isFetchingData + ) { this.fetchData(true); } } @@ -156,15 +182,20 @@ class PublicCollectionsStore extends Reflux.Store { let orderColumn: AssetsTableColumn; if (this.data.orderColumnId) { orderColumn = ASSETS_TABLE_COLUMNS[this.data.orderColumnId]; - const direction = this.data.orderValue === ORDER_DIRECTIONS.ascending ? '' : '-'; + const direction = + this.data.orderValue === ORDER_DIRECTIONS.ascending ? '' : '-'; params.ordering = `${direction}${orderColumn.orderBy}`; } actions.library.searchPublicCollections(params); } - onRouteChange(data: Update) { - if (!this.isInitialised && isPublicCollectionsRoute() && !this.data.isFetchingData) { + onRouteChange(data: RouterState) { + if ( + !this.isInitialised && + isPublicCollectionsRoute() && + !this.data.isFetchingData + ) { this.fetchData(true); } else if ( this.previousPath.startsWith(ROUTES.PUBLIC_COLLECTIONS) === false && diff --git a/jsapp/js/components/library/singleCollectionStore.ts b/jsapp/js/components/library/singleCollectionStore.ts index 7576127439..b3de78185c 100644 --- a/jsapp/js/components/library/singleCollectionStore.ts +++ b/jsapp/js/components/library/singleCollectionStore.ts @@ -1,5 +1,5 @@ import Reflux from 'reflux'; -import type {Update} from 'history'; +import type {RouterState} from '@remix-run/router'; import assetUtils from 'js/assetUtils'; import { getCurrentPath, @@ -19,7 +19,7 @@ import type { SearchAssetsPredefinedParams, } from 'js/dataInterface'; import {ROUTES} from 'js/router/routerConstants'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; import type {AssetTypeName} from 'js/constants'; interface SingleCollectionStoreData { @@ -66,7 +66,8 @@ class SingleCollectionStore extends Reflux.Store { organizations: [], }, orderColumnId: this.DEFAULT_ORDER_COLUMN.id, - orderValue: this.DEFAULT_ORDER_COLUMN.defaultValue || ORDER_DIRECTIONS.ascending, + orderValue: + this.DEFAULT_ORDER_COLUMN.defaultValue || ORDER_DIRECTIONS.ascending, filterColumnId: null, filterValue: null, }; @@ -74,23 +75,49 @@ class SingleCollectionStore extends Reflux.Store { init() { this.setDefaultColumns(); - history.listen(this.onRouteChange.bind(this)); - actions.library.moveToCollection.completed.listen(this.onMoveToCollectionCompleted.bind(this)); - actions.library.subscribeToCollection.completed.listen(this.fetchData.bind(this)); - actions.library.unsubscribeFromCollection.completed.listen(this.fetchData.bind(this)); - actions.resources.loadAsset.completed.listen(this.onAssetChanged.bind(this)); - actions.resources.updateAsset.completed.listen(this.onAssetChanged.bind(this)); - actions.resources.cloneAsset.completed.listen(this.onAssetCreated.bind(this)); - actions.resources.createResource.completed.listen(this.onAssetCreated.bind(this)); - actions.resources.deleteAsset.completed.listen(this.onDeleteAssetCompleted.bind(this)); + setTimeout(() => router!.subscribe(this.onRouteChange.bind(this))); + actions.library.moveToCollection.completed.listen( + this.onMoveToCollectionCompleted.bind(this) + ); + actions.library.subscribeToCollection.completed.listen( + this.fetchData.bind(this) + ); + actions.library.unsubscribeFromCollection.completed.listen( + this.fetchData.bind(this) + ); + actions.resources.loadAsset.completed.listen( + this.onAssetChanged.bind(this) + ); + actions.resources.updateAsset.completed.listen( + this.onAssetChanged.bind(this) + ); + actions.resources.cloneAsset.completed.listen( + this.onAssetCreated.bind(this) + ); + actions.resources.createResource.completed.listen( + this.onAssetCreated.bind(this) + ); + actions.resources.deleteAsset.completed.listen( + this.onDeleteAssetCompleted.bind(this) + ); // Actions unique to a single collection store (overwriting myLibraryStore) - actions.library.searchMyCollectionAssets.started.listen(this.onSearchStarted.bind(this)); - actions.library.searchMyCollectionAssets.completed.listen(this.onSearchCompleted.bind(this)); - actions.library.searchMyCollectionAssets.failed.listen(this.onSearchFailed.bind(this)); - actions.library.searchMyCollectionMetadata.completed.listen(this.onSearchMetadataCompleted.bind(this)); + actions.library.searchMyCollectionAssets.started.listen( + this.onSearchStarted.bind(this) + ); + actions.library.searchMyCollectionAssets.completed.listen( + this.onSearchCompleted.bind(this) + ); + actions.library.searchMyCollectionAssets.failed.listen( + this.onSearchFailed.bind(this) + ); + actions.library.searchMyCollectionMetadata.completed.listen( + this.onSearchMetadataCompleted.bind(this) + ); // startup store after config is ready - actions.permissions.getConfig.completed.listen(this.startupStore.bind(this)); + actions.permissions.getConfig.completed.listen( + this.startupStore.bind(this) + ); } /** @@ -98,14 +125,19 @@ class SingleCollectionStore extends Reflux.Store { * otherwise wait until route changes to a library (see `onRouteChange`) */ startupStore() { - if (!this.isInitialised && isAnyLibraryItemRoute() && !this.data.isFetchingData) { + if ( + !this.isInitialised && + isAnyLibraryItemRoute() && + !this.data.isFetchingData + ) { this.fetchData(true); } } setDefaultColumns() { this.data.orderColumnId = this.DEFAULT_ORDER_COLUMN.id; - this.data.orderValue = this.DEFAULT_ORDER_COLUMN.defaultValue || ORDER_DIRECTIONS.ascending; + this.data.orderValue = + this.DEFAULT_ORDER_COLUMN.defaultValue || ORDER_DIRECTIONS.ascending; this.data.filterColumnId = null; this.data.filterValue = null; } @@ -155,24 +187,27 @@ class SingleCollectionStore extends Reflux.Store { if (this.data.orderColumnId !== null) { const orderColumn = ASSETS_TABLE_COLUMNS[this.data.orderColumnId]; - const direction = this.data.orderValue === ORDER_DIRECTIONS.ascending ? '' : '-'; + const direction = + this.data.orderValue === ORDER_DIRECTIONS.ascending ? '' : '-'; params.ordering = `${direction}${orderColumn.orderBy}`; } actions.library.searchMyCollectionAssets(params); } - onRouteChange(data: Update) { - if (!this.isInitialised && isAnyLibraryItemRoute() && !this.data.isFetchingData) { + onRouteChange(data: RouterState) { + if ( + !this.isInitialised && + isAnyLibraryItemRoute() && + !this.data.isFetchingData + ) { this.fetchData(true); } else if ( - ( - // coming from the library - this.previousPath.split('/')[1] === 'library' || + // coming from the library + (this.previousPath.split('/')[1] === 'library' || // public-collections is a special case that is kinda in library, but // actually outside of it - this.previousPath.startsWith(ROUTES.PUBLIC_COLLECTIONS) - ) && + this.previousPath.startsWith(ROUTES.PUBLIC_COLLECTIONS)) && isAnyLibraryItemRoute() ) { // refresh data when navigating into library from other place @@ -243,12 +278,10 @@ class SingleCollectionStore extends Reflux.Store { const loopAsset = this.data.assets[i]; if ( loopAsset.uid === asset.uid && - ( - // if the changed asset didn't change (e.g. was just loaded) - // let's not cause it to fetchMetadata - loopAsset.date_modified !== asset.date_modified || - loopAsset.version_id !== asset.version_id - ) + // if the changed asset didn't change (e.g. was just loaded) + // let's not cause it to fetchMetadata + (loopAsset.date_modified !== asset.date_modified || + loopAsset.version_id !== asset.version_id) ) { this.data.assets[i] = asset; wasUpdated = true; @@ -263,10 +296,7 @@ class SingleCollectionStore extends Reflux.Store { } onAssetCreated(asset: AssetResponse) { - if ( - assetUtils.isLibraryAsset(asset.asset_type) && - asset.parent === null - ) { + if (assetUtils.isLibraryAsset(asset.asset_type) && asset.parent === null) { if (this.data.totalUserAssets !== null) { this.data.totalUserAssets++; } @@ -274,7 +304,7 @@ class SingleCollectionStore extends Reflux.Store { } } - onDeleteAssetCompleted(response: {uid: string; assetType: AssetTypeName;}) { + onDeleteAssetCompleted(response: {uid: string; assetType: AssetTypeName}) { if (assetUtils.isLibraryAsset(response.assetType)) { const found = this.findAsset(response.uid); if (found) { diff --git a/jsapp/js/components/map.es6 b/jsapp/js/components/map.es6 index c25b19ab91..cf26515488 100644 --- a/jsapp/js/components/map.es6 +++ b/jsapp/js/components/map.es6 @@ -27,46 +27,51 @@ import { QUERY_LIMIT_DEFAULT, } from '../constants'; -import { - notify, - checkLatLng -} from 'utils'; +import {notify, checkLatLng} from 'utils'; import {getSurveyFlatPaths} from 'js/assetUtils'; import MapSettings from './mapSettings'; -var streets = L.tileLayer( +const streets = L.tileLayer( 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap', - subdomains: ['a', 'b', 'c'] + attribution: + '© OpenStreetMap', + subdomains: ['a', 'b', 'c'], } ); -var baseLayers = { +const baseLayers = { OpenStreetMap: streets, OpenTopoMap: L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { - attribution: 'Map data: © OpenStreetMap, SRTM | Map style: © OpenTopoMap (CC-BY-SA)' + attribution: + 'Map data: © OpenStreetMap, SRTM | Map style: © OpenTopoMap (CC-BY-SA)', }), 'ESRI World Imagery': L.tileLayer( - 'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { - attribution: 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community' - }), + 'http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', + { + attribution: + 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community', + } + ), Humanitarian: L.tileLayer( - 'https://tile-{s}.openstreetmap.fr/hot/{z}/{x}/{y}.png', { - attribution: 'Tiles © Humanitarian OpenStreetMap Team — © OpenStreetMap' - }) + 'https://tile-{s}.openstreetmap.fr/hot/{z}/{x}/{y}.png', + { + attribution: + 'Tiles © Humanitarian OpenStreetMap Team — © OpenStreetMap', + } + ), }; -var controls = L.control.layers(baseLayers); +const controls = L.control.layers(baseLayers); export class FormMap extends React.Component { - constructor(props){ + constructor(props) { super(props); - let survey = props.asset.content.survey; - var hasGeoPoint = false; - survey.forEach(function(s) { + const survey = props.asset.content.survey; + let hasGeoPoint = false; + survey.forEach(function (s) { if (s.type === QUESTION_TYPES.geopoint.id) { hasGeoPoint = true; } @@ -96,17 +101,22 @@ export class FormMap extends React.Component { autoBind(this); } - componentWillUnmount () { + componentWillUnmount() { if (this.state.map) { this.state.map.remove(); } } - componentDidMount () { - - var fields = []; - let fieldTypes = ['select_one', 'select_multiple', 'integer', 'decimal', 'text']; - this.props.asset.content.survey.forEach(function(q){ + componentDidMount() { + const fields = []; + const fieldTypes = [ + 'select_one', + 'select_multiple', + 'integer', + 'decimal', + 'text', + ]; + this.props.asset.content.survey.forEach(function (q) { if (fieldTypes.includes(q.type)) { fields.push(q); } @@ -114,47 +124,61 @@ export class FormMap extends React.Component { L.Marker.prototype.options.icon = L.divIcon({ className: 'map-marker default-overlay-marker', - iconSize: [12, 12] + iconSize: [12, 12], }); - var map = L.map('data-map', { + const map = L.map('data-map', { maxZoom: 17, scrollWheelZoom: false, - preferCanvas: true + preferCanvas: true, }); streets.addTo(map); controls.addTo(map); this.setState({ - map: map, - fields: fields - } - ); + map: map, + fields: fields, + }); - if(this.props.asset.deployment__submission_count > QUERY_LIMIT_DEFAULT) { - notify(t('By default map is limited to the ##number## most recent submissions for performance reasons. Go to map settings to increase this limit.').replace('##number##', QUERY_LIMIT_DEFAULT)); + if (this.props.asset.deployment__submission_count > QUERY_LIMIT_DEFAULT) { + notify( + t( + 'By default map is limited to the ##number## most recent submissions for performance reasons. Go to map settings to increase this limit.' + ).replace('##number##', QUERY_LIMIT_DEFAULT) + ); } this.requestData(map, this.props.viewby); this.listenTo(actions.map.setMapStyles.started, this.onSetMapStylesStarted); - this.listenTo(actions.map.setMapStyles.completed, this.onSetMapStylesCompleted); - this.listenTo(actions.resources.getAssetFiles.completed, this.updateOverlayList); - actions.resources.getAssetFiles(this.props.asset.uid, ASSET_FILE_TYPES.map_layer.id); + this.listenTo( + actions.map.setMapStyles.completed, + this.onSetMapStylesCompleted + ); + this.listenTo( + actions.resources.getAssetFiles.completed, + this.updateOverlayList + ); + actions.resources.getAssetFiles( + this.props.asset.uid, + ASSET_FILE_TYPES.map_layer.id + ); } - loadOverlayLayers(map) { + + loadOverlayLayers() { dataInterface .getAssetFiles(this.props.asset.uid, ASSET_FILE_TYPES.map_layer.id) - .done((data) => {}); + .done(() => {}); } + updateOverlayList(data) { - let map = this.state.map; + const map = this.state.map; // remove layers from controls if they are no longer in asset files - controls._layers.forEach(function(controlLayer) { + controls._layers.forEach(function (controlLayer) { if (controlLayer.overlay) { - let layerMatch = data.results.filter( - result => result.name === controlLayer.name + const layerMatch = data.results.filter( + (result) => result.name === controlLayer.name ); if (!layerMatch.length) { controls.removeLayer(controlLayer.layer); @@ -164,14 +188,18 @@ export class FormMap extends React.Component { }); // add new layers to controls (if they haven't been added already) - data.results.forEach(function(layer) { - if (layer.file_type !== 'map_layer') return false; - let layerMatch = controls._layers.filter( - controlLayer => controlLayer.name === layer.name + data.results.forEach(function (layer) { + if (layer.file_type !== 'map_layer') { + return false; + } + const layerMatch = controls._layers.filter( + (controlLayer) => controlLayer.name === layer.name ); - if (layerMatch.length) return false; + if (layerMatch.length) { + return false; + } - var overlayLayer = false; + let overlayLayer = false; switch (layer.metadata.type) { case 'kml': overlayLayer = omnivore.kml(layer.content); @@ -191,35 +219,40 @@ export class FormMap extends React.Component { // unzip the KMZ file in the browser // and feed the resulting text to map and controls fetch(layer.content) - .then(function (response) { - if (response.status === 200 || response.status === 0) { - return Promise.resolve(response.blob()); - } else { - return Promise.reject(new Error(response.statusText)); - } - }) - .then(JSZip.loadAsync) - .then(function (zip) { - return zip.file('doc.kml').async('string'); - }) - .then(function success(kml) { - overlayLayer = omnivore.kml.parse(kml); - controls.addOverlay(overlayLayer, layer.name); - overlayLayer.addTo(map); - }); + .then(function (response) { + if (response.status === 200 || response.status === 0) { + return Promise.resolve(response.blob()); + } else { + return Promise.reject(new Error(response.statusText)); + } + }) + .then(JSZip.loadAsync) + .then(function (zip) { + return zip.file('doc.kml').async('string'); + }) + .then(function success(kml) { + overlayLayer = omnivore.kml.parse(kml); + controls.addOverlay(overlayLayer, layer.name); + overlayLayer.addTo(map); + }); break; } if (overlayLayer) { - overlayLayer.on('ready', function() { - overlayLayer.eachLayer(function(l) { - let fprops = l.feature.properties; - let name = fprops.name || fprops.title || fprops.NAME || fprops.TITLE; + overlayLayer.on('ready', function () { + overlayLayer.eachLayer(function (l) { + const fprops = l.feature.properties; + const name = + fprops.name || fprops.title || fprops.NAME || fprops.TITLE; if (name) { l.bindPopup(name); } else { // when no name or title, load full list of feature's properties - l.bindPopup('
' + JSON.stringify(fprops, null, 2).replace(/[{}"]/g, '') + '
'); + l.bindPopup( + '
' +
+                  JSON.stringify(fprops, null, 2).replace(/[{}"]/g, '') +
+                  '
' + ); } }); }); @@ -255,7 +288,7 @@ export class FormMap extends React.Component { // See: https://github.com/kobotoolbox/kpi/issues/3913 let selectedQuestion = this.props.asset.map_styles.selectedQuestion || null; - this.props.asset.content.survey.forEach(function(row) { + this.props.asset.content.survey.forEach(function (row) { if ( typeof row.label !== 'undefined' && row.label !== null && @@ -273,36 +306,49 @@ export class FormMap extends React.Component { queryLimit = this.props.asset.map_styles.querylimit; } - var fq = ['_id', '_geolocation']; - if (selectedQuestion) fq.push(selectedQuestion); - if (nextViewBy) fq.push(this.nameOfFieldInGroup(nextViewBy)); + const fq = ['_id', '_geolocation']; + if (selectedQuestion) { + fq.push(selectedQuestion); + } + if (nextViewBy) { + fq.push(this.nameOfFieldInGroup(nextViewBy)); + } const sort = [{id: '_id', desc: true}]; - dataInterface.getSubmissions(this.props.asset.uid, queryLimit, 0, sort, fq).done((data) => { - let results = data.results; - if (selectedQuestion) { - results.forEach(function(row, i) { - if (row[selectedQuestion]) { - var coordsArray = row[selectedQuestion].split(' '); - results[i]._geolocation[0] = coordsArray[0]; - results[i]._geolocation[1] = coordsArray[1]; - } - }); - } + dataInterface + .getSubmissions(this.props.asset.uid, queryLimit, 0, sort, fq) + .done((data) => { + const results = data.results; + if (selectedQuestion) { + results.forEach(function (row, i) { + if (row[selectedQuestion]) { + const coordsArray = row[selectedQuestion].split(' '); + results[i]._geolocation[0] = coordsArray[0]; + results[i]._geolocation[1] = coordsArray[1]; + } + }); + } - this.setState({submissions: results}); - this.buildMarkers(map); - this.buildHeatMap(map); - }).fail((error)=>{ - if (error.responseText) - this.setState({error: error.responseText, loading: false}); - else if (error.statusText) - this.setState({error: error.statusText, loading: false}); - else - this.setState({error: t('Error: could not load data.'), loading: false}); - }); + this.setState({submissions: results}); + this.buildMarkers(map); + this.buildHeatMap(map); + }) + .fail((error) => { + if (error.responseText) { + this.setState({error: error.responseText, loading: false}); + } else if (error.statusText) { + this.setState({error: error.statusText, loading: false}); + } else { + this.setState({ + error: t('Error: could not load data.'), + loading: false, + }); + } + }); } calculateClusterRadius(zoom) { - if(zoom >= 12) {return 12;} + if (zoom >= 12) { + return 12; + } return 20; } calcColorSet() { @@ -310,62 +356,82 @@ export class FormMap extends React.Component { if (this.state.overridenStyles && this.state.overridenStyles.colorSet) { colorSet = this.state.overridenStyles.colorSet; } else { - let ms = this.props.asset.map_styles; + const ms = this.props.asset.map_styles; colorSet = ms.colorSet ? ms.colorSet : undefined; } return colorSet; } buildMarkers(map) { - var _this = this, - prepPoints = [], - viewby = this.props.viewby || undefined, - colorSet = this.calcColorSet(), - currentQuestionChoices = []; + const _this = this; + const prepPoints = []; + const viewby = this.props.viewby || undefined; + const colorSet = this.calcColorSet(); + let currentQuestionChoices = []; + let mapMarkers = {}; + let mM = []; if (viewby) { - var mapMarkers = this.prepFilteredMarkers(this.state.submissions, this.props.viewby); - var mM = []; - let choices = this.props.asset.content.choices, - survey = this.props.asset.content.survey; + mapMarkers = this.prepFilteredMarkers( + this.state.submissions, + this.props.viewby + ); + const choices = this.props.asset.content.choices; + const survey = this.props.asset.content.survey; - let question = survey.find(s => s.name === viewby || s.$autoname === viewby); + const question = survey.find( + (s) => s.name === viewby || s.$autoname === viewby + ); if (question && question.type === 'select_one') { - currentQuestionChoices = choices.filter(ch => ch.list_name === question.select_from_list_name); + currentQuestionChoices = choices.filter( + (ch) => ch.list_name === question.select_from_list_name + ); } - Object.keys(mapMarkers).map(function(m, i) { + Object.keys(mapMarkers).map(function (m) { + let choice; if (question && question.type === 'select_one') { - var choice = currentQuestionChoices.find(ch => ch.name === m || ch.$autoname === m); + choice = currentQuestionChoices.find( + (ch) => ch.name === m || ch.$autoname === m + ); } mM.push({ count: mapMarkers[m].count, id: mapMarkers[m].id, labels: choice ? choice.label : undefined, - value: m != 'undefined' ? m : undefined + value: m !== 'undefined' ? m : undefined, }); }); - if (colorSet !== undefined && colorSet !== 'a' && question && question.type == 'select_one') { + if ( + colorSet !== undefined && + colorSet !== 'a' && + question && + question.type === 'select_one' + ) { // sort by question choice order, when using any other color set (only makes sense for select_ones) - mM.sort(function(a, b) { - var aIndex = currentQuestionChoices.findIndex(ch => ch.name === a.value); - var bIndex = currentQuestionChoices.findIndex(ch => ch.name === b.value); + mM.sort(function (a, b) { + const aIndex = currentQuestionChoices.findIndex( + (ch) => ch.name === a.value + ); + const bIndex = currentQuestionChoices.findIndex( + (ch) => ch.name === b.value + ); return aIndex - bIndex; }); } else { // sort by occurrence count - mM.sort(function(a, b) { + mM.sort(function (a, b) { return a.count - b.count; }).reverse(); } // move elements with no data in submission for the disaggregated question to end of marker list - var emptyEl = mM.find(m => m.value === undefined); + const emptyEl = mM.find((m) => m.value === undefined); if (emptyEl) { - mM = mM.filter(m => m !== emptyEl); + mM = mM.filter((m) => m !== emptyEl); mM.push(emptyEl); } this.setState({markerMap: mM}); @@ -373,13 +439,13 @@ export class FormMap extends React.Component { this.setState({markerMap: false}); } - this.state.submissions.forEach(function(item){ - var markerProps = {}; + this.state.submissions.forEach(function (item) { + let markerProps = {}; if (checkLatLng(item._geolocation)) { if (viewby && mM) { - var vb = _this.nameOfFieldInGroup(viewby); - var itemId = item[vb]; - let index = mM.findIndex(m => m.value === itemId); + const vb = _this.nameOfFieldInGroup(viewby); + const itemId = item[vb]; + let index = mM.findIndex((m) => m.value === itemId); // spread indexes to use full colorset gamut if necessary if (colorSet !== undefined && colorSet !== 'a') { @@ -387,15 +453,15 @@ export class FormMap extends React.Component { } markerProps = { - icon: _this.buildIcon(index+1), + icon: _this.buildIcon(index + 1), sId: item._id, - typeId: mapMarkers[itemId].id + typeId: mapMarkers[itemId].id, }; } else { markerProps = { icon: _this.buildIcon(), sId: item._id, - typeId: null + typeId: null, }; } @@ -411,10 +477,10 @@ export class FormMap extends React.Component { markers = L.markerClusterGroup({ maxClusterRadius: this.calculateClusterRadius, disableClusteringAtZoom: 16, - iconCreateFunction: function(cluster) { - var childCount = cluster.getChildCount(); + iconCreateFunction: function (cluster) { + const childCount = cluster.getChildCount(); - var markerClass = 'marker-cluster marker-cluster-'; + let markerClass = 'marker-cluster marker-cluster-'; if (childCount < 10) { markerClass += 'small'; } else if (childCount < 100) { @@ -423,8 +489,12 @@ export class FormMap extends React.Component { markerClass += 'large'; } - return new L.divIcon({ html: '
' + childCount + '
', className: markerClass, iconSize: new L.Point(30, 30) }); - } + return new L.divIcon({ + html: '
' + childCount + '
', + className: markerClass, + iconSize: new L.Point(30, 30), + }); + }, }); markers.addLayers(prepPoints); @@ -432,17 +502,19 @@ export class FormMap extends React.Component { markers.on('click', this.launchSubmissionModal).addTo(map); - if (prepPoints.length > 0 && (!viewby || !this.state.componentRefreshed)) { + if ( + prepPoints.length > 0 && + (!viewby || !this.state.componentRefreshed) + ) { map.fitBounds(markers.getBounds()); - } - if(prepPoints == 0) { + } + if (prepPoints.length === 0) { map.fitBounds([[42.373, -71.124]]); this.setState({noData: true}); } this.setState({ - markers: markers - } - ); + markers: markers, + }); } else { this.setState({error: t('Error: could not load data.'), loading: false}); } @@ -450,25 +522,30 @@ export class FormMap extends React.Component { calculateIconIndex(index, mM) { // use neutral color for items with no set value - if (mM[index] && mM[index].value == undefined) + if (mM[index] && mM[index].value === undefined) { return '-novalue'; + } // if there are submissions with unset values, reset the local marker array // this helps us use the full gamut of colors in the set - var emptyEl = mM.find(m => m.value === undefined); - if (emptyEl) mM = mM.filter(m => m !== emptyEl); + const emptyEl = mM.find((m) => m.value === undefined); + if (emptyEl) { + mM = mM.filter((m) => m !== emptyEl); + } // return regular index for list >= 9 items - if (mM.length >= 9) return index; + if (mM.length >= 9) { + return index; + } // spread index fairly evenly from 1 to 9 when less than 9 items in list - var num = (index / mM.length) * 9.5; + const num = (index / mM.length) * 9.5; return Math.round(num); } buildIcon(index = false) { - let colorSet = this.calcColorSet() || 'a'; - let iconClass = index ? `map-marker-${colorSet}${index}` : 'map-marker-a'; + const colorSet = this.calcColorSet() || 'a'; + const iconClass = index ? `map-marker-${colorSet}${index}` : 'map-marker-a'; return L.divIcon({ className: `map-marker ${iconClass}`, @@ -476,35 +553,36 @@ export class FormMap extends React.Component { }); } - prepFilteredMarkers (data, viewby) { - var markerMap = new Object(); - var vb = this.nameOfFieldInGroup(viewby); - var idcounter = 1; + prepFilteredMarkers(data, viewby) { + const markerMap = new Object(); + const vb = this.nameOfFieldInGroup(viewby); + let idcounter = 1; - data.forEach(function(listitem, i) { - var m = listitem[vb]; + data.forEach(function (listitem) { + const m = listitem[vb]; - if (markerMap[m] == null) { - markerMap[m] = {count: 1, id: idcounter}; - idcounter++; + if (markerMap[m] === undefined) { + markerMap[m] = {count: 1, id: idcounter}; + idcounter++; } else { - markerMap[m]['count'] += 1; + markerMap[m]['count'] += 1; } }); return markerMap; } - buildHeatMap (map) { - var heatmapPoints = []; - this.state.submissions.forEach(function(item){ - if (checkLatLng(item._geolocation)) + buildHeatMap(map) { + const heatmapPoints = []; + this.state.submissions.forEach(function (item) { + if (checkLatLng(item._geolocation)) { heatmapPoints.push([item._geolocation[0], item._geolocation[1], 1]); + } }); - var heatmap = L.heatLayer(heatmapPoints, { + const heatmap = L.heatLayer(heatmapPoints, { minOpacity: 0.25, radius: 20, - blur: 8 + blur: 8, }); if (!this.state.markersVisible) { @@ -513,31 +591,29 @@ export class FormMap extends React.Component { this.setState({heatmap: heatmap}); } - showMarkers () { - var map = this.state.map; + showMarkers() { + const map = this.state.map; map.addLayer(this.state.markers); map.removeLayer(this.state.heatmap); this.setState({ - markersVisible: true - } - ); + markersVisible: true, + }); } showLayerControls() { controls.expand(); } - showHeatmap () { - var map = this.state.map; + showHeatmap() { + const map = this.state.map; map.addLayer(this.state.heatmap); map.removeLayer(this.state.markers); this.setState({ - markersVisible: false - } - ); + markersVisible: false, + }); } - filterMap (evt) { + filterMap(evt) { // roundabout solution for https://github.com/kobotoolbox/kpi/issues/1678 // // when blurEventDisabled prop is set, no blur event takes place in PopoverMenu @@ -545,24 +621,26 @@ export class FormMap extends React.Component { // but when changing question, dropdown needs to be removed, clearDisaggregatedPopover does this via props this.setState({clearDisaggregatedPopover: true}); // reset clearDisaggregatedPopover in order to maintain same behaviour on subsequent clicks - window.setTimeout(()=>{ + window.setTimeout(() => { this.setState({clearDisaggregatedPopover: false}); }, 1000); - let name = evt.target.getAttribute('data-name') || undefined; - if (name != undefined) { - this.props.router.navigate(`/forms/${this.props.asset.uid}/data/map/${name}`); + const name = evt.target.getAttribute('data-name') || undefined; + if (name !== undefined) { + this.props.router.navigate( + `/forms/${this.props.asset.uid}/data/map/${name}` + ); } else { this.props.router.navigate(`/forms/${this.props.asset.uid}/data/map`); } } - filterLanguage (evt) { - let index = evt.target.getAttribute('data-index'); + filterLanguage(evt) { + const index = +evt.target.getAttribute('data-index'); this.setState({langIndex: index}); } static getDerivedStateFromProps(props, state) { const newState = { - previousViewby: props.viewby + previousViewby: props.viewby, }; if (props.viewby !== undefined) { newState.markersVisible = true; @@ -575,54 +653,56 @@ export class FormMap extends React.Component { } componentDidUpdate(prevProps) { if (prevProps.viewby !== this.props.viewby) { - let map = this.refreshMap(); + const map = this.refreshMap(); this.requestData(map, this.props.viewby); } } refreshMap() { - var map = this.state.map; + const map = this.state.map; map.removeLayer(this.state.markers); map.removeLayer(this.state.heatmap); return map; } - launchSubmissionModal (evt) { + launchSubmissionModal(evt) { const td = this.state.submissions; - var ids = []; - td.forEach(function(r) { + const ids = []; + td.forEach(function (r) { ids.push(r._id); - }) + }); stores.pageState.showModal({ type: MODAL_TYPES.SUBMISSION, sid: evt.layer.options.sId, asset: this.props.asset, - ids: ids + ids: ids, }); } toggleMapSettings() { this.setState({ - showMapSettings: !this.state.showMapSettings + showMapSettings: !this.state.showMapSettings, }); } overrideStyles(mapStyles) { this.setState({ filteredByMarker: false, componentRefreshed: true, - overridenStyles: mapStyles + overridenStyles: mapStyles, }); - let map = this.refreshMap(); + const map = this.refreshMap(); // HACK switch to setState callback after updating to React 16+ window.setTimeout(() => { this.requestData(map, this.props.viewby); }, 0); } - toggleFullscreen () { + toggleFullscreen() { this.setState({isFullscreen: !this.state.isFullscreen}); - var map = this.state.map; - setTimeout(function(){ map.invalidateSize()}, 300); + const map = this.state.map; + setTimeout(function () { + map.invalidateSize(); + }, 300); } toggleLegend() { @@ -632,31 +712,33 @@ export class FormMap extends React.Component { } filterByMarker(evt) { - let markers = this.state.markers, - id = evt.target.getAttribute('data-id'), - filteredByMarker = this.state.filteredByMarker, - unselectedClass = 'unselected'; + const markers = this.state.markers; + const id = evt.target.getAttribute('data-id'); + let filteredByMarker = this.state.filteredByMarker; + const unselectedClass = 'unselected'; - if (!filteredByMarker) + if (!filteredByMarker) { filteredByMarker = [id]; - else if (!filteredByMarker.includes(id)) + } else if (!filteredByMarker.includes(id)) { filteredByMarker.push(id); - else - filteredByMarker = filteredByMarker.filter(l => l !== id); + } else { + filteredByMarker = filteredByMarker.filter((l) => l !== id); + } this.setState({filteredByMarker: filteredByMarker}); - markers.eachLayer( function(layer) { - if (!filteredByMarker.includes(layer.options.typeId.toString())) + markers.eachLayer(function (layer) { + if (!filteredByMarker.includes(layer.options.typeId.toString())) { layer._icon.classList.add(unselectedClass); - else + } else { layer._icon.classList.remove(unselectedClass); + } }); } resetFilterByMarker() { - let markers = this.state.markers; + const markers = this.state.markers; this.setState({filteredByMarker: false}); - markers.eachLayer( function(layer) { + markers.eachLayer(function (layer) { layer._icon.classList.remove('unselected'); }); } @@ -666,39 +748,42 @@ export class FormMap extends React.Component { return flatPaths[fieldName]; } - render () { + render() { if (this.state.error) { return ( - - {this.state.error} - + {this.state.error} ); } - const fields = this.state.fields, - langIndex = this.state.langIndex, - langs = this.props.asset.content.translations.length > 1 ? this.props.asset.content.translations : [], - viewby = this.props.viewby; + const fields = this.state.fields; + const langIndex = this.state.langIndex; + const langs = + this.props.asset.content.translations?.length > 1 + ? this.props.asset.content.translations + : []; + const viewby = this.props.viewby; - let colorSet = this.calcColorSet() || 'a'; - var label = t('Disaggregate by survey responses'); + const colorSet = this.calcColorSet() || 'a'; + let label = t('Disaggregate by survey responses'); if (viewby) { - fields.forEach(function(f){ - if(viewby === f.name || viewby === f.$autoname) { + fields.forEach(function (f) { + if (viewby === f.name || viewby === f.$autoname) { label = `${t('Disaggregated using:')} ${f.label[langIndex]}`; } }); } else if (this.state.noData && this.state.hasGeoPoint) { label = `${t('No "geopoint" responses have been received')}`; } else if (!this.state.hasGeoPoint) { - label = `${t('The map does not show data because this form does not have a "geopoint" field.')}` + label = `${t( + 'The map does not show data because this form does not have a "geopoint" field.' + )}`; } const formViewModifiers = ['map']; @@ -708,146 +793,204 @@ export class FormMap extends React.Component { return ( - + className={this.state.toggleFullscreen ? 'active' : ''} + > - + className={this.state.markersVisible ? 'active' : ''} + > - + data-tip={t('Toggle layers')} + > + data-tip={t('Map display settings')} + > - {!viewby && - + className={!this.state.markersVisible ? 'active' : ''} + > - } + )} - { this.state.hasGeoPoint && !this.state.noData && - - {langs.length > 1 && + {this.state.hasGeoPoint && !this.state.noData && ( + + {langs.length > 1 && ( {t('Language')} - } - {langs.map((l,i)=> { - return ( - - {l ? l : t('Default')} - - ); - })} - + )} + {langs.map((l, i) => ( + + {l ? l : t('Default')} + + ))} + {t('-- See all data --')} - {fields.map((f)=>{ + {fields.map((f) => { const name = f.name || f.$autoname; - const label = f.label ? f.label[langIndex] ? f.label[langIndex] : {t('untranslated: ') + name} : t('Question label not set'); + const label = f.label ? ( + f.label[langIndex] ? ( + f.label[langIndex] + ) : ( + {t('untranslated: ') + name} + ) + ) : ( + t('Question label not set') + ); return ( - - {label} - - ); + + {label} + + ); })} + )} - } - - {this.state.noData && !this.state.hasGeoPoint && -
-
-

- {t('The map does not show data because this form does not have a "geopoint" field.')} -

+ {this.state.noData && !this.state.hasGeoPoint && ( +
+
+

+ {t( + 'The map does not show data because this form does not have a "geopoint" field.' + )} +

+
-
- } + )} - {this.state.noData && this.state.hasGeoPoint && -
-
-

- {t('No "geopoint" responses have been received')} -

+ {this.state.noData && this.state.hasGeoPoint && ( +
+
+

+ {t('No "geopoint" responses have been received')} +

+
-
- } + )} - {this.state.markerMap && this.state.markersVisible && - + {this.state.markerMap && this.state.markersVisible && ( +
- {this.state.filteredByMarker && -
+ {this.state.filteredByMarker && ( +
{t('Reset')}
- } - {this.state.markerMap.map((m, i)=>{ - var markerItemClass = 'map-marker-item '; - if (this.state.filteredByMarker) - markerItemClass += this.state.filteredByMarker.includes(m.id.toString()) ? 'selected' : 'unselected'; - let label = m.labels ? m.labels[langIndex] : m.value ? m.value : t('not set'); - var index = i; + )} + {this.state.markerMap.map((m, i) => { + let markerItemClass = 'map-marker-item '; + if (this.state.filteredByMarker) { + markerItemClass += this.state.filteredByMarker.includes( + m.id.toString() + ) + ? 'selected' + : 'unselected'; + } + const label = m.labels + ? m.labels[langIndex] + : m.value + ? m.value + : t('not set'); + let index = i; if (colorSet !== undefined && colorSet !== 'a') { index = this.calculateIconIndex(index, this.state.markerMap); } return ( -
- - {m.count} - - - {label} - -
- ); +
+ + {m.count} + + + {label} + +
+ ); })}
- {t('Legend')} + {' '} + {t('Legend')}
- } - {!this.state.markers && !this.state.heatmap && + )} + {!this.state.markers && !this.state.heatmap && ( - + - } + )} {this.state.showMapSettings && ( + title={t('Map Settings')} + > - ); + ); } } diff --git a/jsapp/js/components/modalForms/projectSettings.es6 b/jsapp/js/components/modalForms/projectSettings.es6 index 193043c958..d0c57c32c4 100644 --- a/jsapp/js/components/modalForms/projectSettings.es6 +++ b/jsapp/js/components/modalForms/projectSettings.es6 @@ -34,7 +34,7 @@ import {ROUTES} from 'js/router/routerConstants'; import {LOCKING_RESTRICTIONS} from 'js/components/locking/lockingConstants'; import {hasAssetRestriction} from 'js/components/locking/lockingUtils'; import envStore from 'js/envStore'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; import {withRouter} from 'js/router/legacy'; import {userCan} from 'js/components/permissions/utils'; @@ -117,7 +117,7 @@ class ProjectSettings extends React.Component { actions.resources.cloneAsset.failed.listen(this.onCloneAssetFailed.bind(this)), actions.resources.setDeploymentActive.failed.listen(this.onSetDeploymentActiveFailed.bind(this)), actions.resources.setDeploymentActive.completed.listen(this.onSetDeploymentActiveCompleted.bind(this)), - history.listen(this.onRouteChange.bind(this)) + router.subscribe(this.onRouteChange.bind(this)) ); } diff --git a/jsapp/js/components/processing/processingUtils.ts b/jsapp/js/components/processing/processingUtils.ts index a36b88c3cf..e4e9e56df6 100644 --- a/jsapp/js/components/processing/processingUtils.ts +++ b/jsapp/js/components/processing/processingUtils.ts @@ -1,7 +1,7 @@ import {ROUTES} from 'js/router/routerConstants'; import {SUPPLEMENTAL_DETAILS_PROP} from 'js/constants'; import type {LanguageCode} from 'js/components/languages/languagesStore'; -import {history} from 'jsapp/js/router/historyRouter'; +import { router } from 'js/router/legacy'; /** * Returns a path that leads to transcription value in the submission response, @@ -54,5 +54,5 @@ export function openProcessing( const route = ROUTES.FORM_PROCESSING.replace(':uid', assetUid) .replace(':qpath', qpath) .replace(':submissionEditId', submissionEditId); - history.push(route); + router!.navigate(route); } diff --git a/jsapp/js/components/processing/singleProcessingRoute.tsx b/jsapp/js/components/processing/singleProcessingRoute.tsx index d55982f68d..27138983ea 100644 --- a/jsapp/js/components/processing/singleProcessingRoute.tsx +++ b/jsapp/js/components/processing/singleProcessingRoute.tsx @@ -10,7 +10,7 @@ import SingleProcessingContent from 'js/components/processing/singleProcessingCo import SingleProcessingPreview from 'js/components/processing/singleProcessingPreview'; import singleProcessingStore from 'js/components/processing/singleProcessingStore'; import {UNSAVED_CHANGES_WARNING} from 'jsapp/js/protector/protectorConstants'; -import {usePrompt} from 'jsapp/js/router/promptBlocker'; +import {unstable_usePrompt as usePrompt} from 'react-router-dom'; import type {WithRouterProps} from 'jsapp/js/router/legacy'; import styles from './singleProcessingRoute.module.scss'; @@ -21,7 +21,7 @@ interface SingleProcessingRouteProps extends WithRouterProps { } const Prompt = () => { - usePrompt(UNSAVED_CHANGES_WARNING); + usePrompt({message: UNSAVED_CHANGES_WARNING, when: true}); return <>; }; diff --git a/jsapp/js/components/processing/singleProcessingStore.ts b/jsapp/js/components/processing/singleProcessingStore.ts index bacb4bd6b9..3d30d3daa8 100644 --- a/jsapp/js/components/processing/singleProcessingStore.ts +++ b/jsapp/js/components/processing/singleProcessingStore.ts @@ -1,12 +1,12 @@ import Reflux from 'reflux'; import alertify from 'alertifyjs'; -import type {Update} from 'history'; +import type {RouterState} from '@remix-run/router'; import {FORM_PROCESSING_BASE} from 'js/router/routerConstants'; import { isFormSingleProcessingRoute, getSingleProcessingRouteParameters, } from 'js/router/routerUtils'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; import { getSurveyFlatPaths, getAssetProcessingRows, @@ -20,9 +20,7 @@ import type {SurveyFlatPaths} from 'js/assetUtils'; import assetStore from 'js/assetStore'; import {actions} from 'js/actions'; import processingActions from 'js/components/processing/processingActions'; -import type { - ProcessingDataResponse, -} from 'js/components/processing/processingActions'; +import type {ProcessingDataResponse} from 'js/components/processing/processingActions'; import type { FailResponse, SubmissionResponse, @@ -171,7 +169,7 @@ class SingleProcessingStore extends Reflux.Store { init() { this.resetProcessingData(); - history.listen(this.onRouteChange.bind(this)); + setTimeout(() => router!.subscribe(this.onRouteChange.bind(this))); actions.submissions.getSubmissionByUuid.completed.listen( this.onGetSubmissionByUuidCompleted.bind(this) @@ -326,7 +324,7 @@ class SingleProcessingStore extends Reflux.Store { } } - private onRouteChange(data: Update) { + private onRouteChange(data: RouterState) { if (this.previousPath === data.location.pathname) { return; } diff --git a/jsapp/js/components/projectDownloads/exportsStore.es6 b/jsapp/js/components/projectDownloads/exportsStore.es6 index e7985eb972..f949af4df6 100644 --- a/jsapp/js/components/projectDownloads/exportsStore.es6 +++ b/jsapp/js/components/projectDownloads/exportsStore.es6 @@ -1,18 +1,17 @@ import Reflux from 'reflux'; import {DEFAULT_EXPORT_SETTINGS} from './exportsConstants'; -import {history} from 'js/router/historyRouter'; +import {router} from 'js/router/legacy'; /** * It handles the selected export type. */ const exportsStore = Reflux.createStore({ - previousPath: history.location.pathname, data: { exportType: DEFAULT_EXPORT_SETTINGS.EXPORT_TYPE, }, init() { - history.listen(this.onRouteChange.bind(this)); + router.subscribe(this.onRouteChange.bind(this)); }, onRouteChange() { @@ -24,11 +23,11 @@ const exportsStore = Reflux.createStore({ }, isOnProjectDownloadsRoute() { - const path = history.location.pathname; + const path = router.state.location.pathname; return ( path.split('/')[1] === 'forms' && path.split('/')[3] === 'data' && - path.split('/')[3] === 'downloads' + path.split('/')[4] === 'downloads' ); }, diff --git a/jsapp/js/components/reports/reports.es6 b/jsapp/js/components/reports/reports.js similarity index 99% rename from jsapp/js/components/reports/reports.es6 rename to jsapp/js/components/reports/reports.js index eb9fc28df1..6adaad7c34 100644 --- a/jsapp/js/components/reports/reports.es6 +++ b/jsapp/js/components/reports/reports.js @@ -122,7 +122,7 @@ export default class Reports extends React.Component { reportStyles: reportStyles, reportData: dataWithResponses, reportCustom: reportCustom, - translations: asset.content.translations.length > 1, + translations: asset.content.translations?.length > 1, groupBy: groupBy, error: false, }); diff --git a/jsapp/js/components/sectionNotFound.es6 b/jsapp/js/components/sectionNotFound.js similarity index 100% rename from jsapp/js/components/sectionNotFound.es6 rename to jsapp/js/components/sectionNotFound.js diff --git a/jsapp/js/components/submissions/tableSettings.es6 b/jsapp/js/components/submissions/tableSettings.es6 index 2b6ddd2019..bdf6aad8d2 100644 --- a/jsapp/js/components/submissions/tableSettings.es6 +++ b/jsapp/js/components/submissions/tableSettings.es6 @@ -75,7 +75,7 @@ class TableSettings extends React.Component { value: -1, label: t('XML Values'), }); - this.props.asset.content.translations.map((trns, n) => { + (this.props.asset.content.translations || [null]).map((trns, n) => { let label = t('Labels'); if (trns) { label += ` - ${trns}`; diff --git a/jsapp/js/editorMixins/editableForm.es6 b/jsapp/js/editorMixins/editableForm.es6 index 37d6f44fac..74e6aee8a0 100644 --- a/jsapp/js/editorMixins/editableForm.es6 +++ b/jsapp/js/editorMixins/editableForm.es6 @@ -46,7 +46,7 @@ import { unnullifyTranslations, } from 'js/components/formBuilder/formBuilderUtils'; import envStore from 'js/envStore'; -import { usePrompt } from 'js/router/promptBlocker'; +import {unstable_usePrompt as usePrompt} from 'react-router-dom'; const ErrorMessage = makeBem(null, 'error-message'); const ErrorMessage__strong = makeBem(null, 'error-message__header', 'strong'); @@ -56,7 +56,7 @@ const WEBFORM_STYLES_SUPPORT_URL = 'alternative_enketo.html'; const UNSAVED_CHANGES_WARNING = t('You have unsaved changes. Leave form without saving?'); /** Use usePrompt directly instead for functional components */ const Prompt = () => { - usePrompt(UNSAVED_CHANGES_WARNING); + usePrompt({when: true, message: UNSAVED_CHANGES_WARNING}); return <>; }; diff --git a/jsapp/js/lists/forms.es6 b/jsapp/js/lists/forms.js similarity index 100% rename from jsapp/js/lists/forms.es6 rename to jsapp/js/lists/forms.js diff --git a/jsapp/js/mixins.tsx b/jsapp/js/mixins.tsx index e058da1f32..67f0b5b286 100644 --- a/jsapp/js/mixins.tsx +++ b/jsapp/js/mixins.tsx @@ -46,8 +46,7 @@ import type { Permission, } from 'js/dataInterface'; import {getRouteAssetUid} from 'js/router/routerUtils'; -import {routerGetAssetId, routerIsActive} from 'js/router/legacy'; -import {history} from 'js/router/historyRouter'; +import {router, routerGetAssetId, routerIsActive} from 'js/router/legacy'; import {userCan} from 'js/components/permissions/utils'; const IMPORT_CHECK_INTERVAL = 1000; @@ -123,12 +122,12 @@ const mixins: MixinsObject = { switch (asset.asset_type) { case ASSET_TYPES.survey.id: - history.push(ROUTES.FORM_LANDING.replace(':uid', asset.uid)); + router!.navigate(ROUTES.FORM_LANDING.replace(':uid', asset.uid)); break; case ASSET_TYPES.template.id: case ASSET_TYPES.block.id: case ASSET_TYPES.question.id: - history.push(ROUTES.LIBRARY); + router!.navigate(ROUTES.LIBRARY); break; } }, @@ -174,7 +173,7 @@ mixins.dmix = { }, { onComplete: (asset: AssetResponse) => { dialog.destroy(); - history.push(`/forms/${asset.uid}`); + router!.navigate(`/forms/${asset.uid}`); }, }); @@ -206,7 +205,7 @@ mixins.dmix = { onDone: () => { notify(t('deployed form')); actions.resources.loadAsset({id: asset.uid}); - history.push(`/forms/${asset.uid}`); + router!.navigate(`/forms/${asset.uid}`); toast.dismiss(deployment_toast); }, onFail: () => { @@ -511,13 +510,13 @@ mixins.droppable = { // TODO: use a more specific error message here notify.error(t('XLSForm Import failed. Check that the XLSForm and/or the URL are valid, and try again using the "Replace form" icon.')); if (params.assetUid) { - history.push(`/forms/${params.assetUid}`); + router.navigate(`/forms/${params.assetUid}`); } } else { if (isProjectReplaceInForm) { actions.resources.loadAsset({id: assetUid}); } else if (!isLibrary) { - history.push(`/forms/${assetUid}`); + router.navigate(`/forms/${assetUid}`); } notify(t('XLS Import completed')); } @@ -642,7 +641,7 @@ mixins.clickAssets = { goToUrl = `/library/asset/${asset.uid}`; } - history.push(goToUrl); + router!.navigate(goToUrl); notify(t('cloned ##ASSET_TYPE## created').replace('##ASSET_TYPE##', assetTypeLabel)); }, }); @@ -675,9 +674,9 @@ mixins.clickAssets = { }, edit: function (uid: string) { if (routerIsActive('library')) { - history.push(`/library/asset/${uid}/edit`); + router!.navigate(`/library/asset/${uid}/edit`); } else { - history.push(`/forms/${uid}/edit`); + router!.navigate(`/forms/${uid}/edit`); } }, delete: function ( diff --git a/jsapp/js/router/allRoutes.es6 b/jsapp/js/router/allRoutes.es6 index c7bb36039c..f445cc33aa 100644 --- a/jsapp/js/router/allRoutes.es6 +++ b/jsapp/js/router/allRoutes.es6 @@ -1,55 +1,13 @@ -import React, {Suspense} from 'react'; +import React from 'react'; import {observer} from 'mobx-react'; import autoBind from 'react-autobind'; -import {Navigate, Routes} from 'react-router-dom'; -import App from 'js/app'; -import {FormPage, LibraryAssetEditor} from 'js/components/formEditors'; +import {RouterProvider} from 'react-router-dom'; import {actions} from 'js/actions'; -import MyLibraryRoute from 'js/components/library/myLibraryRoute'; -import PublicCollectionsRoute from 'js/components/library/publicCollectionsRoute'; -import AssetRoute from 'js/components/library/assetRoute'; -import FormsSearchableList from 'js/lists/forms'; -import SingleProcessingRoute from 'js/components/processing/singleProcessingRoute'; -import {ROUTES} from 'js/router/routerConstants'; import permConfig from 'js/components/permissions/permConfig'; import LoadingSpinner from 'js/components/common/loadingSpinner'; -import {PERMISSIONS_CODENAMES} from 'js/constants'; import {isRootRoute, redirectToLogin} from 'js/router/routerUtils'; -import RequireAuth from 'js/router/requireAuth'; -import PermProtectedRoute from 'js/router/permProtectedRoute'; import sessionStore from 'js/stores/session'; -import {Tracking} from './useTracking'; -import {history} from './historyRouter'; -import accountRoutes from 'js/account/routes'; -import projectsRoutes from 'js/projects/routes'; - -// Workaround https://github.com/remix-run/react-router/issues/8139 -import {unstable_HistoryRouter as HistoryRouter, Route} from 'react-router-dom'; - -const Reports = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/reports/reports') -); -const FormLanding = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/formLanding') -); -const FormSummary = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/formSummary') -); -const FormSubScreens = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/formSubScreens') -); -const FormXform = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/formXform') -); -const FormJson = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/formJson') -); -const SectionNotFound = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/sectionNotFound') -); -const FormNotFound = React.lazy(() => - import(/* webpackPrefetch: true */ 'js/components/formNotFound') -); +import router from './router'; const AllRoutes = class AllRoutes extends React.Component { constructor(props) { @@ -121,352 +79,7 @@ const AllRoutes = class AllRoutes extends React.Component { // redirect is async, continue showing loading return ; } - - return ( - - - - }> - } /> - {accountRoutes()} - {projectsRoutes()} - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - - - - - } - /> - - } - /> - - - } - /> - - - } - /> - - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - - - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - } - /> - - - - } - /> - - } - /> - - } - /> - {/** - * TODO change this HACKFIX to a better solution - * - * Used to force refresh form sub routes. It's some kine of a weird - * way of introducing a loading screen during sub route refresh. - * See: https://github.com/kobotoolbox/kpi/issues/3925 - **/} - - } - /> - - - - - - - } - /> - - - - ); + return ; } }; diff --git a/jsapp/js/router/historyRouter.ts b/jsapp/js/router/historyRouter.ts deleted file mode 100644 index eeb7b044a0..0000000000 --- a/jsapp/js/router/historyRouter.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Workaround for - * https://github.com/remix-run/react-router/issues/8139 - * Also allows for history.listen(this.onRouteChange.bind(this)); - * Don't use this for new code! - */ -import {createHashHistory} from 'history'; - -export const history = createHashHistory({window}); diff --git a/jsapp/js/router/legacy.tsx b/jsapp/js/router/legacy.tsx index b4c9168000..801311710d 100644 --- a/jsapp/js/router/legacy.tsx +++ b/jsapp/js/router/legacy.tsx @@ -9,6 +9,7 @@ import { Location, NavigateFunction, } from 'react-router-dom'; +import type {Router} from '@remix-run/router'; // https://stackoverflow.com/a/70754791/443457 const getRoutePath = (location: Location, params: Params): string => { @@ -38,7 +39,6 @@ interface RouterProp { export interface WithRouterProps { router: RouterProp; params: Readonly>; // Defined as props twice for compat! - } /** @@ -62,7 +62,7 @@ export function withRouter(Component: FC | typeof React.Component) { } function getCurrentRoute() { - return location.hash.split('#')[1] || ''; + return router!.state.location.pathname; } /** @@ -84,3 +84,15 @@ export function routerGetAssetId() { } return null; } + +/** + * Necessary to avoid circular dependency + * Because router may be null, non-component uses may need to check + * null status or use setTimeout to ensure it's run after the first react render cycle + * For modern code, use router hooks instead of this. + * https://github.com/remix-run/react-router/issues/9422#issuecomment-1314642344 + */ +export let router: Router | null = null; +export function injectRouter(newRouter: Router) { + router = newRouter; +} diff --git a/jsapp/js/router/permProtectedRoute.es6 b/jsapp/js/router/permProtectedRoute.js similarity index 100% rename from jsapp/js/router/permProtectedRoute.es6 rename to jsapp/js/router/permProtectedRoute.js diff --git a/jsapp/js/router/promptBlocker.tsx b/jsapp/js/router/promptBlocker.tsx deleted file mode 100644 index 38ca3f4637..0000000000 --- a/jsapp/js/router/promptBlocker.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/** - * These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'. - * Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315 - * Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381 - */ -import {useContext, useEffect, useCallback} from 'react'; -import {UNSAFE_NavigationContext as NavigationContext} from 'react-router-dom'; -/** - * Blocks all navigation attempts. This is useful for preventing the page from - * changing until some condition is met, like saving form data. - * - * @param blocker - * @param when - * @see https://reactrouter.com/api/useBlocker - */ -export function useBlocker(blocker: any, when = true) { - const {navigator} = useContext(NavigationContext); - - useEffect(() => { - if (!when) return; - - const unblock = (navigator as any).block((tx: any) => { - const autoUnblockingTx = { - ...tx, - retry() { - // Automatically unblock the transition so it can play all the way - // through before retrying it. TODO: Figure out how to re-enable - // this block if the transition is cancelled for some reason. - unblock(); - tx.retry(); - }, - }; - - blocker(autoUnblockingTx); - }); - - return unblock; - }, [navigator, blocker, when]); -} -/** - * Prompts the user with an Alert before they leave the current screen. - * - * @param message - * @param when - */ -export function usePrompt(message: string, when = true) { - const blocker = useCallback( - (tx) => { - // eslint-disable-next-line no-alert - if (window.confirm(message)) tx.retry(); - }, - [message] - ); - - useBlocker(blocker, when); -} diff --git a/jsapp/js/router/router.tsx b/jsapp/js/router/router.tsx new file mode 100644 index 0000000000..8de6d67d79 --- /dev/null +++ b/jsapp/js/router/router.tsx @@ -0,0 +1,362 @@ +import React, {Suspense} from 'react'; +import { + Navigate, + Route, + createHashRouter, + createRoutesFromElements, +} from 'react-router-dom'; +import App from 'js/app'; +import {ROUTES} from './routerConstants'; +import accountRoutes from 'js/account/routes'; +import projectsRoutes from 'js/projects/routes'; +import RequireAuth from './requireAuth'; +import {FormPage, LibraryAssetEditor} from 'js/components/formEditors'; +import MyLibraryRoute from 'js/components/library/myLibraryRoute'; +import PublicCollectionsRoute from 'js/components/library/publicCollectionsRoute'; +import AssetRoute from 'js/components/library/assetRoute'; +import FormsSearchableList from 'js/lists/forms'; +import SingleProcessingRoute from 'js/components/processing/singleProcessingRoute'; +import PermProtectedRoute from 'js/router/permProtectedRoute'; +import {PERMISSIONS_CODENAMES} from '../constants'; +import {injectRouter} from './legacy'; + +const Reports = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/reports/reports') +); +const FormLanding = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/formLanding') +); +const FormSummary = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/formSummary') +); +const FormSubScreens = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/formSubScreens') +); +const FormXform = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/formXform') +); +const FormJson = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/formJson') +); +const SectionNotFound = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/sectionNotFound') +); +const FormNotFound = React.lazy( + () => import(/* webpackPrefetch: true */ 'js/components/formNotFound') +); + +export const router = createHashRouter( + createRoutesFromElements( + }> + } + /> + {accountRoutes()} + {projectsRoutes()} + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + + + + + } + /> + + } /> + + + } + /> + + + } + /> + + + } /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + + + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + + + } + /> + + } + /> + + } + /> + {/** + * TODO change this HACKFIX to a better solution + * + * Used to force refresh form sub routes. It's some kind of a weird + * way of introducing a loading screen during sub route refresh. + * See: https://github.com/kobotoolbox/kpi/issues/3925 + * + * NOTE: To make this more noticeable, you can increase the + * timeout in FormViewTabs' triggerRefresh(). + **/} + + } + /> + + + + + } + /> + + + + + } + /> + + ) +); + +injectRouter(router); + +export default router; diff --git a/jsapp/js/router/routerConstants.ts b/jsapp/js/router/routerConstants.ts index 9c20aa2309..68600df7bf 100644 --- a/jsapp/js/router/routerConstants.ts +++ b/jsapp/js/router/routerConstants.ts @@ -45,6 +45,5 @@ export const ROUTES = Object.freeze({ FORM_RECORDS: '/forms/:uid/settings/records', FORM_REST: '/forms/:uid/settings/rest', FORM_REST_HOOK: '/forms/:uid/settings/rest/:hookUid', - FORM_KOBOCAT: '/forms/:uid/settings/kobocat', FORM_RESET: '/forms/:uid/reset', }); diff --git a/jsapp/js/router/routerUtils.ts b/jsapp/js/router/routerUtils.ts index fd2fc009e9..f3d8742337 100644 --- a/jsapp/js/router/routerUtils.ts +++ b/jsapp/js/router/routerUtils.ts @@ -158,10 +158,6 @@ export function isFormRestHookRoute(uid: string, hookUid: string): boolean { return getCurrentPath() === ROUTES.FORM_REST_HOOK.replace(':uid', uid).replace(':hookUid', hookUid); } -export function isFormKobocatRoute(uid: string): boolean { - return getCurrentPath() === ROUTES.FORM_KOBOCAT.replace(':uid', uid); -} - export function isFormSingleProcessingRoute( uid: string, qpath: string, diff --git a/package-lock.json b/package-lock.json index c86017e02e..e544807ef2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,6 @@ "backbone-validation": "^0.11.5", "classnames": "^2.3.1", "fuse.js": "^6.4.3", - "history": "^5.3.0", "immutable": "^3.8.2", "jquery": "^3.5.1", "jquery-ui": "1.12.1", @@ -60,7 +59,7 @@ "react-infinite-scroller": "^1.2.6", "react-mixin": "^5.0.0", "react-modal": "^3.15.1", - "react-router-dom": "^6.4.2", + "react-router-dom": "~6.14.2", "react-select": "^5.3.0", "react-table": "^6.8.1", "react-tagsinput": "^3.19.0", @@ -3582,9 +3581,9 @@ "dev": true }, "node_modules/@remix-run/router": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.2.tgz", - "integrity": "sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", + "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==", "engines": { "node": ">=14" } @@ -18512,14 +18511,6 @@ "he": "bin/he" } }, - "node_modules/history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "dependencies": { - "@babel/runtime": "^7.7.6" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "license": "BSD-3-Clause", @@ -23587,11 +23578,11 @@ } }, "node_modules/react-router": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.2.tgz", - "integrity": "sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz", + "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==", "dependencies": { - "@remix-run/router": "1.0.2" + "@remix-run/router": "1.7.2" }, "engines": { "node": ">=14" @@ -23601,12 +23592,12 @@ } }, "node_modules/react-router-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.2.tgz", - "integrity": "sha512-yM1kjoTkpfjgczPrcyWrp+OuQMyB1WleICiiGfstnQYo/S8hPEEnVjr/RdmlH6yKK4Tnj1UGXFSa7uwAtmDoLQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz", + "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==", "dependencies": { - "@remix-run/router": "1.0.2", - "react-router": "6.4.2" + "@remix-run/router": "1.7.2", + "react-router": "6.14.2" }, "engines": { "node": ">=14" @@ -30748,9 +30739,9 @@ } }, "@remix-run/router": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.0.2.tgz", - "integrity": "sha512-GRSOFhJzjGN+d4sKHTMSvNeUPoZiDHWmRnXfzaxrqe7dE/Nzlc8BiMSJdLDESZlndM7jIUrZ/F4yWqVYlI0rwQ==" + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", + "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==" }, "@sinclair/typebox": { "version": "0.27.8", @@ -41388,14 +41379,6 @@ "version": "1.2.0", "dev": true }, - "history": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/history/-/history-5.3.0.tgz", - "integrity": "sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==", - "requires": { - "@babel/runtime": "^7.7.6" - } - }, "hoist-non-react-statics": { "version": "3.3.2", "requires": { @@ -44935,20 +44918,20 @@ } }, "react-router": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.4.2.tgz", - "integrity": "sha512-Rb0BAX9KHhVzT1OKhMvCDMw776aTYM0DtkxqUBP8dNBom3mPXlfNs76JNGK8wKJ1IZEY1+WGj+cvZxHVk/GiKw==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz", + "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==", "requires": { - "@remix-run/router": "1.0.2" + "@remix-run/router": "1.7.2" } }, "react-router-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.4.2.tgz", - "integrity": "sha512-yM1kjoTkpfjgczPrcyWrp+OuQMyB1WleICiiGfstnQYo/S8hPEEnVjr/RdmlH6yKK4Tnj1UGXFSa7uwAtmDoLQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz", + "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==", "requires": { - "@remix-run/router": "1.0.2", - "react-router": "6.4.2" + "@remix-run/router": "1.7.2", + "react-router": "6.14.2" } }, "react-select": { diff --git a/package.json b/package.json index d1987c0044..849028f0b0 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "backbone-validation": "^0.11.5", "classnames": "^2.3.1", "fuse.js": "^6.4.3", - "history": "^5.3.0", "immutable": "^3.8.2", "jquery": "^3.5.1", "jquery-ui": "1.12.1", @@ -57,7 +56,7 @@ "react-infinite-scroller": "^1.2.6", "react-mixin": "^5.0.0", "react-modal": "^3.15.1", - "react-router-dom": "^6.4.2", + "react-router-dom": "~6.14.2", "react-select": "^5.3.0", "react-table": "^6.8.1", "react-tagsinput": "^3.19.0",