From a3ada9888d40e6ca944cf2f64fec55fbaed0b606 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Thu, 18 Apr 2024 09:54:50 +0200 Subject: [PATCH 1/6] fix: remove d2 --- src/AppWrapper.js | 116 ++-- src/actions/current.js | 2 +- src/actions/index.js | 6 +- src/actions/ui.js | 2 +- src/actions/visualization.js | 2 +- src/components/App.js | 654 +++++++++--------- src/components/DetailsPanel/DetailsPanel.js | 49 +- .../DimensionsPanel/Dialogs/DialogManager.js | 15 +- .../DimensionsPanel/DimensionsPanel.js | 17 +- .../ModalDownloadDropdown.module.css | 1 - .../InterpretationModal.js | 14 +- src/components/MenuBar/MenuBar.js | 13 +- src/components/UserSettingsProvider.js | 57 -- src/components/Visualization/Visualization.js | 1 + .../VisualizationTypeSelector.js | 19 +- src/modules/fields/baseFields.js | 1 - src/modules/systemSettings.js | 23 + src/modules/userSettings.js | 5 + src/reducers/user.js | 15 +- 19 files changed, 491 insertions(+), 521 deletions(-) delete mode 100644 src/components/UserSettingsProvider.js create mode 100644 src/modules/systemSettings.js create mode 100644 src/modules/userSettings.js diff --git a/src/AppWrapper.js b/src/AppWrapper.js index df06cbd256..75990f8817 100644 --- a/src/AppWrapper.js +++ b/src/AppWrapper.js @@ -1,21 +1,81 @@ -import { useConfig, useDataEngine } from '@dhis2/app-runtime' -import { D2Shim } from '@dhis2/app-runtime-adapter-d2' +import { CachedDataQueryProvider } from '@dhis2/analytics' +import { useDataEngine } from '@dhis2/app-runtime' import { DataStoreProvider } from '@dhis2/app-service-datastore' import React from 'react' import { Provider as ReduxProvider } from 'react-redux' import thunk from 'redux-thunk' -import { App } from './components/App.js' -import UserSettingsProvider, { - UserSettingsCtx, -} from './components/UserSettingsProvider.js' +import App from './components/App.js' import configureStore from './configureStore.js' import metadataMiddleware from './middleware/metadata.js' import { USER_DATASTORE_NAMESPACE } from './modules/currentAnalyticalObject.js' -import history from './modules/history.js' +import { systemSettingsKeys } from './modules/systemSettings.js' +import { + USER_SETTINGS_DISPLAY_PROPERTY, + DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY, +} from './modules/userSettings.js' import './locales/index.js' +const query = { + currentUser: { + resource: 'me', + params: { + fields: 'id,username,displayName~rename(name),settings', + }, + }, + systemSettings: { + resource: 'systemSettings', + params: { + key: systemSettingsKeys, + }, + }, + rootOrgUnits: { + resource: 'organisationUnits', + params: { + fields: 'id,displayName,name', + userDataViewFallback: true, + paging: false, + }, + }, + orgUnitLevels: { + resource: 'organisationUnitLevels', + // TODO how to handle passing params like this? + params: ({ displayNameProp = 'displayName' } = {}) => ({ + fields: `id,level,${displayNameProp}~rename(displayName),name`, + paging: false, + }), + }, +} + +const providerDataTransformation = ({ + currentUser, + systemSettings, + rootOrgUnits, + orgUnitLevels, +}) => { + const displayNameProperty = + currentUser.settings[USER_SETTINGS_DISPLAY_PROPERTY] === 'name' + ? 'displayName' + : 'displayShortName' + + return { + currentUser: { + ...currentUser, + settings: { + uiLocale: currentUser.settings.keyUiLocale, + displayProperty: + currentUser.settings[USER_SETTINGS_DISPLAY_PROPERTY], + displayNameProperty, + [DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY]: + displayNameProperty, + }, + }, + systemSettings, + rootOrgUnits: rootOrgUnits.organisationUnits, + orgUnitLevels: orgUnitLevels.organisationUnitLevels, + } +} + const AppWrapper = () => { - const { baseUrl } = useConfig() const engine = useDataEngine() const store = configureStore([ thunk.withExtraArgument(engine), @@ -26,43 +86,15 @@ const AppWrapper = () => { window.store = store } - const schemas = ['visualization', 'organisationUnit', 'userGroup'] - const d2Config = { - schemas, - } - return ( - - - {({ userSettings }) => { - return userSettings?.uiLocale ? ( - - {({ d2 }) => { - if (!d2) { - // TODO: Handle errors in d2 initialization - return null - } else { - return ( - - ) - } - }} - - ) : null - }} - - + + + ) diff --git a/src/actions/current.js b/src/actions/current.js index f950cf47e3..d7db8efce4 100644 --- a/src/actions/current.js +++ b/src/actions/current.js @@ -11,7 +11,7 @@ export const acSetCurrent = (value) => ({ value, }) -export const acClear = () => ({ +export const acClearCurrent = () => ({ type: CLEAR_CURRENT, }) diff --git a/src/actions/index.js b/src/actions/index.js index 46a7576726..98ba629bb6 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -130,15 +130,15 @@ export const clearAll = dispatch(fromLoader.acClearLoadError()) } - dispatch(fromVisualization.acClear()) - dispatch(fromCurrent.acClear()) + dispatch(fromVisualization.acClearVisualization()) + dispatch(fromCurrent.acClearCurrent()) const rootOrganisationUnits = sGetRootOrgUnits(getState()) const relativePeriod = sGetRelativePeriod(getState()) const digitGroupSeparator = sGetSettingsDigitGroupSeparator(getState()) dispatch( - fromUi.acClear({ + fromUi.acClearUi({ rootOrganisationUnits, relativePeriod, digitGroupSeparator, diff --git a/src/actions/ui.js b/src/actions/ui.js index 5d1f04e8c4..890d8335ea 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -34,7 +34,7 @@ export const acSetUi = (value) => ({ value, }) -export const acClear = (value) => ({ +export const acClearUi = (value) => ({ type: CLEAR_UI, value, }) diff --git a/src/actions/visualization.js b/src/actions/visualization.js index 86a80ce800..c4ff28ed9e 100644 --- a/src/actions/visualization.js +++ b/src/actions/visualization.js @@ -14,6 +14,6 @@ export const acSetVisualization = (visualization) => { } } -export const acClear = () => ({ +export const acClearVisualization = () => ({ type: CLEAR_VISUALIZATION, }) diff --git a/src/components/App.js b/src/components/App.js index 4a7f88efb7..339c3da348 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,4 +1,4 @@ -import { apiFetchOrganisationUnitLevels, Toolbar } from '@dhis2/analytics' +import { useCachedDataQuery, Toolbar } from '@dhis2/analytics' import { useSetting } from '@dhis2/app-service-datastore' import i18n from '@dhis2/d2-i18n' import { @@ -10,17 +10,28 @@ import { ButtonStrip, Button, } from '@dhis2/ui' -import PropTypes from 'prop-types' -import React, { Component } from 'react' -import { connect } from 'react-redux' -import * as fromActions from '../actions/index.js' +import React, { useEffect, useCallback, useRef, useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + acClearCurrent, + acSetCurrentFromUi, + tSetCurrentFromUi, +} from '../actions/current.js' +import { tSetDimensions } from '../actions/dimensions.js' +import { clearAll, tDoLoadVisualization } from '../actions/index.js' +import { acAddMetadata } from '../actions/metadata.js' +import { acAddSettings } from '../actions/settings.js' +import { acAddParentGraphMap, acSetUiFromVisualization } from '../actions/ui.js' +import { acReceivedUser, tLoadUserAuthority } from '../actions/user.js' +import { acClearVisualization } from '../actions/visualization.js' import { Snackbar } from '../components/Snackbar/Snackbar.js' import { USER_DATASTORE_CURRENT_AO_KEY } from '../modules/currentAnalyticalObject.js' import history from '../modules/history.js' import defaultMetadata from '../modules/metadata.js' import { getParentGraphMapFromVisualization } from '../modules/ui.js' import { STATE_DIRTY, getVisualizationState } from '../modules/visualization.js' -import * as fromReducers from '../reducers/index.js' +import { sGetCurrent } from '../reducers/current.js' +import { sGetUi } from '../reducers/ui.js' import { sGetVisualization } from '../reducers/visualization.js' import { default as DetailsPanel } from './DetailsPanel/DetailsPanel.js' import DimensionsPanel from './DimensionsPanel/DimensionsPanel.js' @@ -35,374 +46,331 @@ import { VisualizationTypeSelector } from './VisualizationTypeSelector/Visualiza import './App.css' import './scrollbar.css' -export class UnconnectedApp extends Component { - unlisten = null - - apiObjectName = 'visualization' - - interpretationsUnitRef = React.createRef() +const App = () => { + const [currentAO] = useSetting(USER_DATASTORE_CURRENT_AO_KEY) - onInterpretationUpdate = () => - this.interpretationsUnitRef.current?.refresh() + const [previousLocation, setPreviousLocation] = useState(null) + const [initialLoadIsComplete, setInitialLoadIsComplete] = useState(false) + const [locationToConfirm, setLocationToConfirm] = useState(false) - state = { - previousLocation: null, - initialLoadIsComplete: false, - locationToConfirm: false, + const dispatch = useDispatch() - ouLevels: null, - } - - fetchOuLevels = async () => { - const ouLevels = await apiFetchOrganisationUnitLevels( - this.props.dataEngine - ) + const current = useSelector(sGetCurrent) + const ui = useSelector(sGetUi) + const visualization = useSelector(sGetVisualization) - this.setState({ ouLevels: ouLevels }) - } + const { currentUser, systemSettings, orgUnitLevels, rootOrgUnits } = + useCachedDataQuery() - /** - * The following cases require a fetch/refetch of the AO - * - enter a new url (causing a page load) - * - file->open (same or different AO) - * - file->saveAs - */ - refetch = (location) => { - if (!this.state.previousLocation) { - return true - } - - const id = location.pathname.slice(1).split('/')[0] - const prevId = this.state.previousLocation.slice(1).split('/')[0] - - if ( - id !== prevId || - this.state.previousLocation === location.pathname - ) { - return true - } - - return false + const interpretationsUnitRef = useRef() + const onInterpretationUpdate = () => { + interpretationsUnitRef.current.refresh() } - parseLocation = (location) => { + const parseLocation = (location) => { const pathParts = location.pathname.slice(1).split('/') const id = pathParts[0] const interpretationId = pathParts[2] return { id, interpretationId } } - loadVisualization = async (location) => { - if (location.pathname.length > 1) { - // /currentAnalyticalObject - // /${id}/ - // /${id}/interpretation/${interpretationId} - const { id } = this.parseLocation(location) - - const urlContainsCurrentAOKey = id === USER_DATASTORE_CURRENT_AO_KEY - - if (urlContainsCurrentAOKey) { - this.props.addParentGraphMap( - getParentGraphMapFromVisualization(this.props.currentAO) - ) + const loadVisualization = useCallback( + (location) => { + /** + * The following cases require a fetch/refetch of the AO + * - enter a new url (causing a page load) + * - file->open (same or different AO) + * - file->saveAs + */ + const isRefetchNeeded = (location) => { + if (!previousLocation) { + return true + } - // clear visualization and current - // to avoid leave them "dirty" when navigating to - // /currentAnalyticalObject from a previously saved AO - this.props.clearVisualization() - this.props.clearCurrent() + const id = location.pathname.slice(1).split('/')[0] + const prevId = previousLocation.slice(1).split('/')[0] - this.props.setUiFromVisualization(this.props.currentAO) - this.props.setCurrentFromUi() - } + if (id !== prevId || previousLocation === location.pathname) { + return true + } - if (!urlContainsCurrentAOKey && this.refetch(location)) { - await this.props.setVisualization({ - id, - ouLevels: this.state.ouLevels, - }) + return false } - } else { - this.props.clearAll() - } - this.setState({ initialLoadIsComplete: true }) - this.setState({ previousLocation: location.pathname }) - } - componentDidMount = async () => { - const { d2, userSettings } = this.props + if (location.pathname.length > 1) { + // /currentAnalyticalObject + // /${id}/ + // /${id}/interpretation/${interpretationId} + const { id } = parseLocation(location) + + const urlContainsCurrentAOKey = + id === USER_DATASTORE_CURRENT_AO_KEY + + if (urlContainsCurrentAOKey) { + dispatch( + acAddParentGraphMap( + getParentGraphMapFromVisualization(currentAO) + ) + ) + + // clear visualization and current + // to avoid leave them "dirty" when navigating to + // /currentAnalyticalObject from a previously saved AO + dispatch(acClearVisualization()) + dispatch(acClearCurrent()) + + dispatch(acSetUiFromVisualization(currentAO)) + dispatch(tSetCurrentFromUi()) + } - await this.props.addSettings(userSettings) - this.props.setUser(d2.currentUser) - this.props.loadUserAuthority(APPROVAL_LEVEL_OPTION_AUTH) - this.props.setDimensions() + if (!urlContainsCurrentAOKey && isRefetchNeeded(location)) { + dispatch( + tDoLoadVisualization({ + id, + ouLevels: orgUnitLevels, + }) + ) + } + } else { + dispatch(clearAll()) // XXX + } + setInitialLoadIsComplete(true) + setPreviousLocation(location.pathname) + }, + [ + currentAO, + dispatch, + orgUnitLevels, + previousLocation, + setPreviousLocation, + ] + ) + + useEffect( + () => { + dispatch( + // XXX see how to write this better + acAddSettings({ + ...systemSettings, + uiLocale: currentUser.settings.uiLocale, + displayProperty: currentUser.settings.displayProperty, + displayNameProperty: + currentUser.settings.displayNameProperty, + rootOrganisationUnits: rootOrgUnits, + }) + ) + dispatch(tLoadUserAuthority('ALL')) + dispatch(tLoadUserAuthority(APPROVAL_LEVEL_OPTION_AUTH)) + dispatch(acReceivedUser(currentUser)) + dispatch(tSetDimensions()) + + const metaData = { ...defaultMetadata() } + + rootOrgUnits.forEach((rootOrgUnit) => { + if (rootOrgUnit.id) { + metaData[rootOrgUnit.id] = { + ...rootOrgUnit, + path: `/${rootOrgUnit.id}`, + } + } + }) - await this.fetchOuLevels() + dispatch(acAddMetadata(metaData)) - const rootOrgUnits = this.props.settings.rootOrganisationUnits + loadVisualization(history.location) - const metaData = { ...defaultMetadata() } + const unlisten = history.listen(({ location }) => { + const isSaving = location.state?.isSaving + const isOpening = location.state?.isOpening + const isResetting = location.state?.isResetting + const isModalOpening = location.state?.isModalOpening + const isModalClosing = location.state?.isModalClosing + const isValidLocationChange = + previousLocation !== location.pathname && + !isModalOpening && + !isModalClosing - rootOrgUnits.forEach((rootOrgUnit) => { - if (rootOrgUnit.id) { - metaData[rootOrgUnit.id] = { - ...rootOrgUnit, - path: `/${rootOrgUnit.id}`, - } - } - }) - - this.props.addMetadata(metaData) - - this.loadVisualization(this.props.location) - - this.unlisten = history.listen(({ location }) => { - const isSaving = location.state?.isSaving - const isOpening = location.state?.isOpening - const isResetting = location.state?.isResetting - /* - const isModalOpening = location.state?.isModalOpening - const isModalClosing = location.state?.isModalClosing - const isValidLocationChange = - this.state.previousLocation !== location.pathname && - !isModalOpening && - !isModalClosing -*/ - if ( - // currently editing - getVisualizationState( - this.props.visualization, - this.props.current - ) === STATE_DIRTY && - // wanting to navigate elsewhere - this.state.previousLocation !== location.pathname && - // not saving - !isSaving - ) { - this.setState({ locationToConfirm: location }) - } else { if ( - isSaving || - isOpening || - isResetting || - this.state.previousLocation !== location.pathname + // currently editing + getVisualizationState(visualization, current) === + STATE_DIRTY && + // wanting to navigate elsewhere + previousLocation !== location.pathname && + // not saving + !isSaving ) { - this.loadVisualization(location) + setLocationToConfirm(location) + } else { + if ( + isSaving || + isOpening || + isResetting || + isValidLocationChange + ) { + loadVisualization(location) + } + + setLocationToConfirm(null) } + }) - this.setState({ locationToConfirm: null }) - } - }) - - document.body.addEventListener( - 'keyup', - (e) => - e.key === 'Enter' && - e.ctrlKey === true && - this.props.setCurrentFromUi() - ) - - window.addEventListener('beforeunload', (event) => { - if ( - getVisualizationState( - this.props.visualization, - this.props.current - ) === STATE_DIRTY - ) { - event.preventDefault() - event.returnValue = i18n.t('You have unsaved changes.') - } - }) - } - - componentWillUnmount() { - if (this.unlisten) { - this.unlisten() - } - } + document.body.addEventListener( + 'keyup', + (e) => + e.key === 'Enter' && + e.ctrlKey === true && + dispatch(acSetCurrentFromUi(ui)) + ) - getChildContext() { - return { - baseUrl: this.props.baseUrl, - i18n, - d2: this.props.d2, - dataEngine: this.props.dataEngine, - } - } - - render() { - return ( - <> -
- - - - -
- -
- + window.addEventListener('beforeunload', (event) => { + if ( + getVisualizationState(visualization, current) === + STATE_DIRTY + ) { + event.preventDefault() + event.returnValue = i18n.t('You have unsaved changes.') + } + }) + + return () => unlisten && unlisten() + }, + [ + // current, + // currentUser, + // dispatch, + // loadVisualization, + // previousLocation, + // rootOrgUnits, + // systemSettings, + // ui, + // visualization, + ] + ) + + // TODO continue from here + + // this.unlisten = history.listen(({ location }) => { + // const isSaving = location.state?.isSaving + // const isOpening = location.state?.isOpening + // const isResetting = location.state?.isResetting + // /* + // const isModalOpening = location.state?.isModalOpening + // const isModalClosing = location.state?.isModalClosing + // const isValidLocationChange = + // this.state.previousLocation !== location.pathname && + // !isModalOpening && + // !isModalClosing + //*/ + // if ( + // // currently editing + // getVisualizationState( + // this.props.visualization, + // this.props.current + // ) === STATE_DIRTY && + // // wanting to navigate elsewhere + // this.state.previousLocation !== location.pathname && + // // not saving + // !isSaving + // ) { + // this.setState({ locationToConfirm: location }) + // } else { + // if ( + // isSaving || + // isOpening || + // isResetting || + // this.state.previousLocation !== location.pathname + // ) { + // this.loadVisualization(location) + // } + // + // this.setState({ locationToConfirm: null }) + // } + // }) + + return ( + <> +
+ + + + +
+ +
+ +
+
+
+
-
-
- -
-
- -
-
- {this.state.initialLoadIsComplete && ( - - )} - {this.props.current && ( - - )} -
+
+
- - {this.props.ui.rightSidebarOpen && this.props.current && ( -
- +
+ {initialLoadIsComplete && } + {current && ( + + )}
- )} -
+
+ + {ui.rightSidebarOpen && current && ( +
+ +
+ )}
- {this.state.locationToConfirm && ( - - - {i18n.t('Discard unsaved changes?')} - - - {i18n.t( - 'Are you sure you want to leave this visualization? Any unsaved changes will be lost.' - )} - - - - - - - - - - )} - - - - ) - } -} - -const mapStateToProps = (state) => ({ - settings: fromReducers.fromSettings.sGetSettings(state), - current: fromReducers.fromCurrent.sGetCurrent(state), - ui: fromReducers.fromUi.sGetUi(state), - visualization: sGetVisualization(state), - snackbar: fromReducers.fromSnackbar.sGetSnackbar(state), -}) - -const mapDispatchToProps = { - setCurrentFromUi: fromActions.fromCurrent.tSetCurrentFromUi, - clearVisualization: fromActions.fromVisualization.acClear, - clearCurrent: fromActions.fromCurrent.acClear, - setUiFromVisualization: fromActions.fromUi.acSetUiFromVisualization, - addParentGraphMap: fromActions.fromUi.acAddParentGraphMap, - clearSnackbar: fromActions.fromSnackbar.acClearSnackbar, - addSettings: fromActions.fromSettings.tAddSettings, - setUser: fromActions.fromUser.acReceivedUser, - loadUserAuthority: fromActions.fromUser.tLoadUserAuthority, - setDimensions: fromActions.fromDimensions.tSetDimensions, - addMetadata: fromActions.fromMetadata.acAddMetadata, - setVisualization: fromActions.tDoLoadVisualization, - clearAll: fromActions.clearAll, -} - -UnconnectedApp.contextTypes = { - store: PropTypes.object, -} - -UnconnectedApp.childContextTypes = { - d2: PropTypes.object, - dataEngine: PropTypes.object, - baseUrl: PropTypes.string, - i18n: PropTypes.object, -} - -UnconnectedApp.propTypes = { - addMetadata: PropTypes.func, - addParentGraphMap: PropTypes.func, - addSettings: PropTypes.func, - baseUrl: PropTypes.string, - clearAll: PropTypes.func, - clearCurrent: PropTypes.func, - clearVisualization: PropTypes.func, - current: PropTypes.object, - currentAO: PropTypes.object, - d2: PropTypes.object, - dataEngine: PropTypes.object, - loadUserAuthority: PropTypes.func, - location: PropTypes.object, - setCurrentFromUi: PropTypes.func, - setDimensions: PropTypes.func, - setUiFromVisualization: PropTypes.func, - setUser: PropTypes.func, - setVisualization: PropTypes.func, - settings: PropTypes.object, - ui: PropTypes.object, - userSettings: PropTypes.object, - visualization: PropTypes.object, -} - -const withCurrentAO = (Component) => { - return function WrappedComponent(props) { - const [currentAO] = useSetting(USER_DATASTORE_CURRENT_AO_KEY) - - return - } +
+ {locationToConfirm && ( + + + {i18n.t('Discard unsaved changes?')} + + + {i18n.t( + 'Are you sure you want to leave this visualization? Any unsaved changes will be lost.' + )} + + + + + + + + + + )} + + + + ) } -export const App = connect( - mapStateToProps, - mapDispatchToProps -)(withCurrentAO(UnconnectedApp)) +export default App diff --git a/src/components/DetailsPanel/DetailsPanel.js b/src/components/DetailsPanel/DetailsPanel.js index 604a69f72c..6807404d38 100644 --- a/src/components/DetailsPanel/DetailsPanel.js +++ b/src/components/DetailsPanel/DetailsPanel.js @@ -1,4 +1,8 @@ -import { AboutAOUnit, InterpretationsUnit } from '@dhis2/analytics' +import { + AboutAOUnit, + InterpretationsUnit, + useCachedDataQuery, +} from '@dhis2/analytics' import PropTypes from 'prop-types' import { stringify } from 'query-string' import React from 'react' @@ -18,30 +22,27 @@ const navigateToOpenModal = (interpretationId, initialFocus) => { ) } -const DetailsPanel = ( - { interpretationsUnitRef, visualization, disabled }, - context -) => ( -
- - - navigateToOpenModal(interpretationId) - } - onReplyIconClick={(interpretationId) => - navigateToOpenModal(interpretationId, true) - } - disabled={disabled} - /> -
-) +const DetailsPanel = ({ interpretationsUnitRef, visualization, disabled }) => { + const { currentUser } = useCachedDataQuery() -DetailsPanel.contextTypes = { - d2: PropTypes.object, + return ( +
+ + + navigateToOpenModal(interpretationId) + } + onReplyIconClick={(interpretationId) => + navigateToOpenModal(interpretationId, true) + } + disabled={disabled} + /> +
+ ) } DetailsPanel.propTypes = { diff --git a/src/components/DimensionsPanel/Dialogs/DialogManager.js b/src/components/DimensionsPanel/Dialogs/DialogManager.js index a7f59bbea4..5fe0e57151 100644 --- a/src/components/DimensionsPanel/Dialogs/DialogManager.js +++ b/src/components/DimensionsPanel/Dialogs/DialogManager.js @@ -57,11 +57,6 @@ import { } from '../../../modules/ui.js' import { sGetDimensions } from '../../../reducers/dimensions.js' import { sGetMetadata } from '../../../reducers/metadata.js' -import { - sGetRootOrgUnits, - sGetSettings, - sGetSettingsDisplayNameProperty, -} from '../../../reducers/settings.js' import { sGetUiItems, sGetUiItemsByDimension, @@ -125,7 +120,7 @@ export class DialogManager extends Component { fetchRecommended = debounce(async () => { const ids = await apiFetchRecommendedIds( - this.context.dataEngine, + this.props.dataEngine, this.props.dxIds, this.props.ouIds ) @@ -533,12 +528,9 @@ export class DialogManager extends Component { } } -DialogManager.contextTypes = { - dataEngine: PropTypes.object, -} - DialogManager.propTypes = { changeDialog: PropTypes.func.isRequired, + dataEngine: PropTypes.object.isRequired, dimensionIdsInLayout: PropTypes.array.isRequired, ouIds: PropTypes.array.isRequired, setRecommendedIds: PropTypes.func.isRequired, @@ -567,16 +559,13 @@ DialogManager.defaultProps = { } const mapStateToProps = (state) => ({ - displayNameProperty: sGetSettingsDisplayNameProperty(state), dialogId: sGetUiActiveModalDialog(state), dimensions: sGetDimensions(state), metadata: sGetMetadata(state), parentGraphMap: sGetUiParentGraphMap(state), dxIds: sGetUiItemsByDimension(state, DIMENSION_ID_DATA), ouIds: sGetUiItemsByDimension(state, DIMENSION_ID_ORGUNIT), - rootOrgUnits: sGetRootOrgUnits(state), selectedItems: sGetUiItems(state), - settings: sGetSettings(state), type: sGetUiType(state), getAxisIdByDimensionId: (dimensionId) => sGetAxisIdByDimensionId(state, dimensionId), diff --git a/src/components/DimensionsPanel/DimensionsPanel.js b/src/components/DimensionsPanel/DimensionsPanel.js index 3fbb4408df..fe1fbc8333 100644 --- a/src/components/DimensionsPanel/DimensionsPanel.js +++ b/src/components/DimensionsPanel/DimensionsPanel.js @@ -3,7 +3,9 @@ import { DIMENSION_ID_ASSIGNED_CATEGORIES, DIMENSION_ID_DATA, VIS_TYPE_SCATTER, + useCachedDataQuery, } from '@dhis2/analytics' +import { useDataEngine } from '@dhis2/app-runtime' import { Layer, Popper } from '@dhis2/ui' import PropTypes from 'prop-types' import React, { useState } from 'react' @@ -14,6 +16,7 @@ import { acRemoveUiLayoutDimensions, } from '../../actions/ui.js' import { ITEM_ATTRIBUTE_VERTICAL } from '../../modules/ui.js' +import { DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY } from '../../modules/userSettings.js' import * as fromReducers from '../../reducers/index.js' import { default as DialogManager } from './Dialogs/DialogManager.js' import { default as DndDimensionsPanel } from './DndDimensionsPanel.js' @@ -29,10 +32,13 @@ export const Dimensions = ({ ui, onDimensionClick, }) => { + const dataEngine = useDataEngine() const [menuIsOpen, setMenuIsOpen] = useState(false) const [dimensionId, setDimensionId] = useState(null) const [ref, setRef] = useState() + const { rootOrgUnits, systemSettings, currentUser } = useCachedDataQuery() + const toggleMenu = () => { if (menuIsOpen) { setDimensionId(null) @@ -89,7 +95,16 @@ export const Dimensions = ({ )} - +
) } diff --git a/src/components/DownloadMenu/ModalDownloadDropdown.module.css b/src/components/DownloadMenu/ModalDownloadDropdown.module.css index 55c5ecaf22..99816df267 100644 --- a/src/components/DownloadMenu/ModalDownloadDropdown.module.css +++ b/src/components/DownloadMenu/ModalDownloadDropdown.module.css @@ -1,4 +1,3 @@ .container { margin-top: var(--spacers-dp12); - margin-bottom: var(--spacers-dp16); } diff --git a/src/components/InterpretationModal/InterpretationModal.js b/src/components/InterpretationModal/InterpretationModal.js index 2af15b452e..96430ec105 100644 --- a/src/components/InterpretationModal/InterpretationModal.js +++ b/src/components/InterpretationModal/InterpretationModal.js @@ -1,4 +1,7 @@ -import { InterpretationModal as AnalyticsInterpretationModal } from '@dhis2/analytics' +import { + InterpretationModal as AnalyticsInterpretationModal, + useCachedDataQuery, +} from '@dhis2/analytics' import PropTypes from 'prop-types' import React, { useState, useEffect, useCallback } from 'react' import { useSelector } from 'react-redux' @@ -10,7 +13,8 @@ import { removeInterpretationQueryParams, } from './interpretationIdQueryParam.js' -const InterpretationModal = ({ onInterpretationUpdate }, context) => { +const InterpretationModal = ({ onInterpretationUpdate }) => { + const { currentUser } = useCachedDataQuery() const { interpretationId, initialFocus } = useInterpretationQueryParams() const [isVisualizationLoading, setIsVisualizationLoading] = useState(false) const visualization = useSelector(sGetVisualization) @@ -25,7 +29,7 @@ const InterpretationModal = ({ onInterpretationUpdate }, context) => { return interpretationId ? ( { ) : null } -InterpretationModal.contextTypes = { - d2: PropTypes.object, -} - InterpretationModal.propTypes = { onInterpretationUpdate: PropTypes.func.isRequired, } diff --git a/src/components/MenuBar/MenuBar.js b/src/components/MenuBar/MenuBar.js index add7d03de8..e515d2b6ef 100644 --- a/src/components/MenuBar/MenuBar.js +++ b/src/components/MenuBar/MenuBar.js @@ -1,9 +1,10 @@ import { FileMenu, HoverMenuBar, + UpdateButton, VIS_TYPE_GROUP_ALL, VIS_TYPE_GROUP_CHARTS, - UpdateButton, + useCachedDataQuery, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' import PropTypes from 'prop-types' @@ -49,7 +50,9 @@ const getOnSaveAs = (props) => (details) => const getOnDelete = (props) => () => props.onDeleteVisualization() const getOnError = (props) => (error) => props.onError(error) -const UnconnectedMenuBar = ({ dataTest, ...props }, context) => { +const UnconnectedMenuBar = ({ dataTest, ...props }) => { + const { currentUser } = useCachedDataQuery() + const filterVisTypesByVersion = useVisTypesFilterByVersion() const filterVisTypes = [ @@ -72,7 +75,7 @@ const UnconnectedMenuBar = ({ dataTest, ...props }, context) => { /> ({ current: sGetCurrent(state), visualization: sGetVisualization(state), diff --git a/src/components/UserSettingsProvider.js b/src/components/UserSettingsProvider.js deleted file mode 100644 index 5575535ebb..0000000000 --- a/src/components/UserSettingsProvider.js +++ /dev/null @@ -1,57 +0,0 @@ -import { useDataEngine } from '@dhis2/app-runtime' -import PropTypes from 'prop-types' -import React, { useContext, useState, useEffect, createContext } from 'react' - -export const userSettingsQuery = { - resource: 'userSettings', - params: { - key: ['keyUiLocale', 'keyAnalysisDisplayProperty'], - }, -} - -export const UserSettingsCtx = createContext({}) - -const UserSettingsProvider = ({ children }) => { - const [settings, setSettings] = useState([]) - const engine = useDataEngine() - - useEffect(() => { - async function fetchData() { - const { userSettings } = await engine.query({ - userSettings: userSettingsQuery, - }) - - const { keyAnalysisDisplayProperty, keyUiLocale, ...rest } = - userSettings - - setSettings({ - ...rest, - displayProperty: keyAnalysisDisplayProperty, - displayNameProperty: - keyAnalysisDisplayProperty === 'name' - ? 'displayName' - : 'displayShortName', - uiLocale: keyUiLocale, - }) - } - fetchData() - }, [engine]) - - return ( - - {children} - - ) -} - -UserSettingsProvider.propTypes = { - children: PropTypes.node, -} - -export default UserSettingsProvider - -export const useUserSettings = () => useContext(UserSettingsCtx) diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index f25e90ee08..a5d336db4c 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -2,6 +2,7 @@ import { DIMENSION_ID_DATA, VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_PIVOT_TABLE, + useCachedDataQuery, } from '@dhis2/analytics' import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' diff --git a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js index 0d00f2ea43..8f913dadeb 100644 --- a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js +++ b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.js @@ -1,4 +1,5 @@ import { visTypeDisplayNames, ToolbarSidebar } from '@dhis2/analytics' +import { useConfig } from '@dhis2/app-runtime' import { useSetting } from '@dhis2/app-service-datastore' import i18n from '@dhis2/d2-i18n' import { Divider, Popper, Layer } from '@dhis2/ui' @@ -27,11 +28,15 @@ import VisualizationTypeListItem from './VisualizationTypeListItem.js' export const MAPS_APP_URL = 'dhis-web-maps' -const UnconnectedVisualizationTypeSelector = ( - { visualizationType, ui, setUi, onItemClick, current, metadata }, - context -) => { - const baseUrl = context.baseUrl +const UnconnectedVisualizationTypeSelector = ({ + visualizationType, + ui, + setUi, + onItemClick, + current, + metadata, +}) => { + const { baseUrl } = useConfig() const filterVisTypesByVersion = useVisTypesFilterByVersion() const [, /* actual value not used */ { set }] = useSetting( @@ -150,10 +155,6 @@ UnconnectedVisualizationTypeSelector.propTypes = { onItemClick: PropTypes.func, } -UnconnectedVisualizationTypeSelector.contextTypes = { - baseUrl: PropTypes.string, -} - const mapStateToProps = (state) => ({ visualizationType: sGetUiType(state), current: sGetCurrent(state), diff --git a/src/modules/fields/baseFields.js b/src/modules/fields/baseFields.js index a96ab2f166..2210edc112 100644 --- a/src/modules/fields/baseFields.js +++ b/src/modules/fields/baseFields.js @@ -100,7 +100,6 @@ export const fieldsByType = { getFieldObject('filters'), getFieldObject('hideSubtitle', { option: true }), getFieldObject('hideTitle', { option: true }), - getFieldObject('href'), // required for translations via analytics FileMenu and d2-ui TranslationDialog getFieldObject('id'), getFieldObject('interpretations'), getFieldObject('itemOrganisationUnitGroups', { excluded: true }), diff --git a/src/modules/systemSettings.js b/src/modules/systemSettings.js new file mode 100644 index 0000000000..f58126c0cf --- /dev/null +++ b/src/modules/systemSettings.js @@ -0,0 +1,23 @@ +const SYSTEM_SETTING_DATE_FORMAT = 'keyDateFormat' +const SYSTEM_SETTINGS_RELATIVE_PERIOD = 'keyAnalysisRelativePeriod' +export const SYSTEM_SETTINGS_DIGIT_GROUP_SEPARATOR = + 'keyAnalysisDigitGroupSeparator' +export const SYSTEM_SETTINGS_HIDE_DAILY_PERIODS = 'keyHideDailyPeriods' +export const SYSTEM_SETTINGS_HIDE_WEEKLY_PERIODS = 'keyHideWeeklyPeriods' +export const SYSTEM_SETTINGS_HIDE_BIWEEKLY_PERIODS = 'keyHideBiWeeklyPeriods' +export const SYSTEM_SETTINGS_HIDE_MONTHLY_PERIODS = 'keyHideMonthlyPeriods' +export const SYSTEM_SETTINGS_HIDE_BIMONTHLY_PERIODS = 'keyHideBiMonthlyPeriods' +export const SYSTEM_SETTINGS_IGNORE_ANALYTICS_APPROVAL_YEAR_THRESHOLD = + 'keyIgnoreAnalyticsApprovalYearThreshold' + +export const systemSettingsKeys = [ + SYSTEM_SETTING_DATE_FORMAT, + SYSTEM_SETTINGS_RELATIVE_PERIOD, + SYSTEM_SETTINGS_DIGIT_GROUP_SEPARATOR, + SYSTEM_SETTINGS_HIDE_DAILY_PERIODS, + SYSTEM_SETTINGS_HIDE_WEEKLY_PERIODS, + SYSTEM_SETTINGS_HIDE_BIWEEKLY_PERIODS, + SYSTEM_SETTINGS_HIDE_MONTHLY_PERIODS, + SYSTEM_SETTINGS_HIDE_BIMONTHLY_PERIODS, + SYSTEM_SETTINGS_IGNORE_ANALYTICS_APPROVAL_YEAR_THRESHOLD, +] diff --git a/src/modules/userSettings.js b/src/modules/userSettings.js new file mode 100644 index 0000000000..cb4899d9c6 --- /dev/null +++ b/src/modules/userSettings.js @@ -0,0 +1,5 @@ +export const USER_SETTINGS_UI_LOCALE = 'keyUiLocale' +export const USER_SETTINGS_DISPLAY_PROPERTY = 'keyAnalysisDisplayProperty' + +export const DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY = + 'DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY' diff --git a/src/reducers/user.js b/src/reducers/user.js index aea4ab1b4a..951af8ea97 100644 --- a/src/reducers/user.js +++ b/src/reducers/user.js @@ -4,15 +4,13 @@ export const SET_USER_AUTHORITY = 'SET_USER_AUTHORITY' export const DEFAULT_USER = { id: '', username: '', - uiLocale: '', - isSuperuser: false, authorities: {}, } export default (state = DEFAULT_USER, action) => { switch (action.type) { case RECEIVED_USER: { - return fromD2ToUserObj(action.value) + return formatUserObject(action.value) } case SET_USER_AUTHORITY: { return { @@ -28,12 +26,10 @@ export default (state = DEFAULT_USER, action) => { } } -function fromD2ToUserObj(d2Object) { +function formatUserObject(userObject) { return { - id: d2Object.id, - username: d2Object.username, - uiLocale: d2Object.settings.keyUiLocale, - isSuperuser: d2Object.authorities.has('ALL'), + id: userObject.id, + username: userObject.username, } } @@ -43,6 +39,5 @@ export const sGetUser = (state) => state.user export const sGetUserId = (state) => sGetUser(state).id export const sGetUsername = (state) => sGetUser(state).username -export const sGetIsSuperuser = (state) => sGetUser(state).isSuperuser -export const sGetUiLocale = (state) => sGetUser(state).uiLocale +export const sGetIsSuperuser = (state) => sGetUser(state).authoritie.has('ALL') export const sGetUserAuthorities = (state) => sGetUser(state).authorities From bbf9adde57fd571a3a9199686f3c69addd934883 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 8 Jul 2024 15:07:05 +0200 Subject: [PATCH 2/6] refactor: switch back to class component The confirm screen when leaving the page didn't work with functional components. --- src/AppWrapper.js | 2 +- src/components/App.js | 646 ++++++++++++++++++++++-------------------- 2 files changed, 337 insertions(+), 311 deletions(-) diff --git a/src/AppWrapper.js b/src/AppWrapper.js index 75990f8817..5087b20815 100644 --- a/src/AppWrapper.js +++ b/src/AppWrapper.js @@ -4,7 +4,7 @@ import { DataStoreProvider } from '@dhis2/app-service-datastore' import React from 'react' import { Provider as ReduxProvider } from 'react-redux' import thunk from 'redux-thunk' -import App from './components/App.js' +import { App } from './components/App.js' import configureStore from './configureStore.js' import metadataMiddleware from './middleware/metadata.js' import { USER_DATASTORE_NAMESPACE } from './modules/currentAnalyticalObject.js' diff --git a/src/components/App.js b/src/components/App.js index 339c3da348..2afffcbda7 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -10,28 +10,17 @@ import { ButtonStrip, Button, } from '@dhis2/ui' -import React, { useEffect, useCallback, useRef, useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { - acClearCurrent, - acSetCurrentFromUi, - tSetCurrentFromUi, -} from '../actions/current.js' -import { tSetDimensions } from '../actions/dimensions.js' -import { clearAll, tDoLoadVisualization } from '../actions/index.js' -import { acAddMetadata } from '../actions/metadata.js' -import { acAddSettings } from '../actions/settings.js' -import { acAddParentGraphMap, acSetUiFromVisualization } from '../actions/ui.js' -import { acReceivedUser, tLoadUserAuthority } from '../actions/user.js' -import { acClearVisualization } from '../actions/visualization.js' +import PropTypes from 'prop-types' +import React, { Component } from 'react' +import { connect } from 'react-redux' +import * as fromActions from '../actions/index.js' import { Snackbar } from '../components/Snackbar/Snackbar.js' import { USER_DATASTORE_CURRENT_AO_KEY } from '../modules/currentAnalyticalObject.js' import history from '../modules/history.js' import defaultMetadata from '../modules/metadata.js' import { getParentGraphMapFromVisualization } from '../modules/ui.js' import { STATE_DIRTY, getVisualizationState } from '../modules/visualization.js' -import { sGetCurrent } from '../reducers/current.js' -import { sGetUi } from '../reducers/ui.js' +import * as fromReducers from '../reducers/index.js' import { sGetVisualization } from '../reducers/visualization.js' import { default as DetailsPanel } from './DetailsPanel/DetailsPanel.js' import DimensionsPanel from './DimensionsPanel/DimensionsPanel.js' @@ -46,331 +35,368 @@ import { VisualizationTypeSelector } from './VisualizationTypeSelector/Visualiza import './App.css' import './scrollbar.css' -const App = () => { - const [currentAO] = useSetting(USER_DATASTORE_CURRENT_AO_KEY) +export class UnconnectedApp extends Component { + unlisten = null - const [previousLocation, setPreviousLocation] = useState(null) - const [initialLoadIsComplete, setInitialLoadIsComplete] = useState(false) - const [locationToConfirm, setLocationToConfirm] = useState(false) + apiObjectName = 'visualization' - const dispatch = useDispatch() + interpretationsUnitRef = React.createRef() - const current = useSelector(sGetCurrent) - const ui = useSelector(sGetUi) - const visualization = useSelector(sGetVisualization) + onInterpretationUpdate = () => + this.interpretationsUnitRef.current?.refresh() - const { currentUser, systemSettings, orgUnitLevels, rootOrgUnits } = - useCachedDataQuery() + state = { + previousLocation: null, + initialLoadIsComplete: false, + locationToConfirm: false, + } - const interpretationsUnitRef = useRef() - const onInterpretationUpdate = () => { - interpretationsUnitRef.current.refresh() + /** + * The following cases require a fetch/refetch of the AO + * - enter a new url (causing a page load) + * - file->open (same or different AO) + * - file->saveAs + */ + refetch = (location) => { + if (!this.state.previousLocation) { + return true + } + + const id = location.pathname.slice(1).split('/')[0] + const prevId = this.state.previousLocation.slice(1).split('/')[0] + + if ( + id !== prevId || + this.state.previousLocation === location.pathname + ) { + return true + } + + return false } - const parseLocation = (location) => { + parseLocation = (location) => { const pathParts = location.pathname.slice(1).split('/') const id = pathParts[0] const interpretationId = pathParts[2] return { id, interpretationId } } - const loadVisualization = useCallback( - (location) => { - /** - * The following cases require a fetch/refetch of the AO - * - enter a new url (causing a page load) - * - file->open (same or different AO) - * - file->saveAs - */ - const isRefetchNeeded = (location) => { - if (!previousLocation) { - return true - } + loadVisualization = async (location) => { + if (location.pathname.length > 1) { + // /currentAnalyticalObject + // /${id}/ + // /${id}/interpretation/${interpretationId} + const { id } = this.parseLocation(location) - const id = location.pathname.slice(1).split('/')[0] - const prevId = previousLocation.slice(1).split('/')[0] + const urlContainsCurrentAOKey = id === USER_DATASTORE_CURRENT_AO_KEY - if (id !== prevId || previousLocation === location.pathname) { - return true - } + if (urlContainsCurrentAOKey) { + this.props.addParentGraphMap( + getParentGraphMapFromVisualization(this.props.currentAO) + ) - return false - } + // clear visualization and current + // to avoid leave them "dirty" when navigating to + // /currentAnalyticalObject from a previously saved AO + this.props.clearVisualization() + this.props.clearCurrent() - if (location.pathname.length > 1) { - // /currentAnalyticalObject - // /${id}/ - // /${id}/interpretation/${interpretationId} - const { id } = parseLocation(location) - - const urlContainsCurrentAOKey = - id === USER_DATASTORE_CURRENT_AO_KEY - - if (urlContainsCurrentAOKey) { - dispatch( - acAddParentGraphMap( - getParentGraphMapFromVisualization(currentAO) - ) - ) - - // clear visualization and current - // to avoid leave them "dirty" when navigating to - // /currentAnalyticalObject from a previously saved AO - dispatch(acClearVisualization()) - dispatch(acClearCurrent()) - - dispatch(acSetUiFromVisualization(currentAO)) - dispatch(tSetCurrentFromUi()) - } - - if (!urlContainsCurrentAOKey && isRefetchNeeded(location)) { - dispatch( - tDoLoadVisualization({ - id, - ouLevels: orgUnitLevels, - }) - ) - } - } else { - dispatch(clearAll()) // XXX + this.props.setUiFromVisualization(this.props.currentAO) + this.props.setCurrentFromUi() } - setInitialLoadIsComplete(true) - setPreviousLocation(location.pathname) - }, - [ - currentAO, - dispatch, - orgUnitLevels, - previousLocation, - setPreviousLocation, - ] - ) - - useEffect( - () => { - dispatch( - // XXX see how to write this better - acAddSettings({ - ...systemSettings, - uiLocale: currentUser.settings.uiLocale, - displayProperty: currentUser.settings.displayProperty, - displayNameProperty: - currentUser.settings.displayNameProperty, - rootOrganisationUnits: rootOrgUnits, + + if (!urlContainsCurrentAOKey && this.refetch(location)) { + await this.props.setVisualization({ + id, + ouLevels: this.props.ouLevels, }) - ) - dispatch(tLoadUserAuthority('ALL')) - dispatch(tLoadUserAuthority(APPROVAL_LEVEL_OPTION_AUTH)) - dispatch(acReceivedUser(currentUser)) - dispatch(tSetDimensions()) - - const metaData = { ...defaultMetadata() } - - rootOrgUnits.forEach((rootOrgUnit) => { - if (rootOrgUnit.id) { - metaData[rootOrgUnit.id] = { - ...rootOrgUnit, - path: `/${rootOrgUnit.id}`, - } - } - }) + } + } else { + this.props.clearAll() + } + this.setState({ initialLoadIsComplete: true }) + this.setState({ previousLocation: location.pathname }) + } + + componentDidMount = async () => { + const { currentUser, userSettings } = this.props - dispatch(acAddMetadata(metaData)) + await this.props.addSettings(userSettings) + this.props.setUser(currentUser) + this.props.loadUserAuthority(APPROVAL_LEVEL_OPTION_AUTH) + this.props.setDimensions() - loadVisualization(history.location) + const rootOrgUnits = this.props.settings.rootOrganisationUnits - const unlisten = history.listen(({ location }) => { - const isSaving = location.state?.isSaving - const isOpening = location.state?.isOpening - const isResetting = location.state?.isResetting - const isModalOpening = location.state?.isModalOpening - const isModalClosing = location.state?.isModalClosing - const isValidLocationChange = - previousLocation !== location.pathname && - !isModalOpening && - !isModalClosing + const metaData = { ...defaultMetadata() } + rootOrgUnits.forEach((rootOrgUnit) => { + if (rootOrgUnit.id) { + metaData[rootOrgUnit.id] = { + ...rootOrgUnit, + path: `/${rootOrgUnit.id}`, + } + } + }) + + this.props.addMetadata(metaData) + + this.loadVisualization(history.location) + + this.unlisten = history.listen(({ location }) => { + const isSaving = location.state?.isSaving + const isOpening = location.state?.isOpening + const isResetting = location.state?.isResetting + /* + const isModalOpening = location.state?.isModalOpening + const isModalClosing = location.state?.isModalClosing + const isValidLocationChange = + this.state.previousLocation !== location.pathname && + !isModalOpening && + !isModalClosing +*/ + if ( + // currently editing + getVisualizationState( + this.props.visualization, + this.props.current + ) === STATE_DIRTY && + // wanting to navigate elsewhere + this.state.previousLocation !== location.pathname && + // not saving + !isSaving + ) { + this.setState({ locationToConfirm: location }) + } else { if ( - // currently editing - getVisualizationState(visualization, current) === - STATE_DIRTY && - // wanting to navigate elsewhere - previousLocation !== location.pathname && - // not saving - !isSaving + isSaving || + isOpening || + isResetting || + this.state.previousLocation !== location.pathname ) { - setLocationToConfirm(location) - } else { - if ( - isSaving || - isOpening || - isResetting || - isValidLocationChange - ) { - loadVisualization(location) - } - - setLocationToConfirm(null) + this.loadVisualization(location) } - }) - document.body.addEventListener( - 'keyup', - (e) => - e.key === 'Enter' && - e.ctrlKey === true && - dispatch(acSetCurrentFromUi(ui)) - ) + this.setState({ locationToConfirm: null }) + } + }) + + document.body.addEventListener( + 'keyup', + (e) => + e.key === 'Enter' && + e.ctrlKey === true && + this.props.setCurrentFromUi() + ) + + window.addEventListener('beforeunload', (event) => { + if ( + getVisualizationState( + this.props.visualization, + this.props.current + ) === STATE_DIRTY + ) { + event.preventDefault() + event.returnValue = i18n.t('You have unsaved changes.') + } + }) + } - window.addEventListener('beforeunload', (event) => { - if ( - getVisualizationState(visualization, current) === - STATE_DIRTY - ) { - event.preventDefault() - event.returnValue = i18n.t('You have unsaved changes.') - } - }) - - return () => unlisten && unlisten() - }, - [ - // current, - // currentUser, - // dispatch, - // loadVisualization, - // previousLocation, - // rootOrgUnits, - // systemSettings, - // ui, - // visualization, - ] - ) - - // TODO continue from here - - // this.unlisten = history.listen(({ location }) => { - // const isSaving = location.state?.isSaving - // const isOpening = location.state?.isOpening - // const isResetting = location.state?.isResetting - // /* - // const isModalOpening = location.state?.isModalOpening - // const isModalClosing = location.state?.isModalClosing - // const isValidLocationChange = - // this.state.previousLocation !== location.pathname && - // !isModalOpening && - // !isModalClosing - //*/ - // if ( - // // currently editing - // getVisualizationState( - // this.props.visualization, - // this.props.current - // ) === STATE_DIRTY && - // // wanting to navigate elsewhere - // this.state.previousLocation !== location.pathname && - // // not saving - // !isSaving - // ) { - // this.setState({ locationToConfirm: location }) - // } else { - // if ( - // isSaving || - // isOpening || - // isResetting || - // this.state.previousLocation !== location.pathname - // ) { - // this.loadVisualization(location) - // } - // - // this.setState({ locationToConfirm: null }) - // } - // }) - - return ( - <> -
- - - - -
- -
- -
-
-
- + componentWillUnmount() { + if (this.unlisten) { + this.unlisten() + } + } + + getChildContext() { + return { + baseUrl: this.props.baseUrl, + i18n, + dataEngine: this.props.dataEngine, + } + } + + render() { + return ( + <> +
+ + + + +
+ +
+
-
- +
+
+ +
+
+ +
+
+ {this.state.initialLoadIsComplete && ( + + )} + {this.props.current && ( + + )} +
-
- {initialLoadIsComplete && } - {current && ( - - )} + + {this.props.ui.rightSidebarOpen && this.props.current && ( +
+
-
- - {ui.rightSidebarOpen && current && ( -
- -
- )} -
-
- {locationToConfirm && ( - - - {i18n.t('Discard unsaved changes?')} - - - {i18n.t( - 'Are you sure you want to leave this visualization? Any unsaved changes will be lost.' )} - - - - - - - - - - )} - - - - ) +
+
+ {this.state.locationToConfirm && ( + + + {i18n.t('Discard unsaved changes?')} + + + {i18n.t( + 'Are you sure you want to leave this visualization? Any unsaved changes will be lost.' + )} + + + + + + + + + + )} + + + + ) + } +} + +const mapStateToProps = (state) => ({ + settings: fromReducers.fromSettings.sGetSettings(state), + current: fromReducers.fromCurrent.sGetCurrent(state), + ui: fromReducers.fromUi.sGetUi(state), + visualization: sGetVisualization(state), + snackbar: fromReducers.fromSnackbar.sGetSnackbar(state), +}) + +const mapDispatchToProps = { + setCurrentFromUi: fromActions.fromCurrent.tSetCurrentFromUi, + clearVisualization: fromActions.fromVisualization.acClearVisualization, + clearCurrent: fromActions.fromCurrent.acClearCurrent, + setUiFromVisualization: fromActions.fromUi.acSetUiFromVisualization, + addParentGraphMap: fromActions.fromUi.acAddParentGraphMap, + clearSnackbar: fromActions.fromSnackbar.acClearSnackbar, + addSettings: fromActions.fromSettings.tAddSettings, + setUser: fromActions.fromUser.acReceivedUser, + loadUserAuthority: fromActions.fromUser.tLoadUserAuthority, + setDimensions: fromActions.fromDimensions.tSetDimensions, + addMetadata: fromActions.fromMetadata.acAddMetadata, + setVisualization: fromActions.tDoLoadVisualization, + clearAll: fromActions.clearAll, +} + +UnconnectedApp.contextTypes = { + store: PropTypes.object, +} + +UnconnectedApp.childContextTypes = { + dataEngine: PropTypes.object, + baseUrl: PropTypes.string, + i18n: PropTypes.object, +} + +UnconnectedApp.propTypes = { + addMetadata: PropTypes.func, + addParentGraphMap: PropTypes.func, + addSettings: PropTypes.func, + baseUrl: PropTypes.string, + clearAll: PropTypes.func, + clearCurrent: PropTypes.func, + clearVisualization: PropTypes.func, + current: PropTypes.object, + currentAO: PropTypes.object, + currentUser: PropTypes.object, + dataEngine: PropTypes.object, + loadUserAuthority: PropTypes.func, + ouLevels: PropTypes.array, + setCurrentFromUi: PropTypes.func, + setDimensions: PropTypes.func, + setUiFromVisualization: PropTypes.func, + setUser: PropTypes.func, + setVisualization: PropTypes.func, + settings: PropTypes.object, + ui: PropTypes.object, + userSettings: PropTypes.object, + visualization: PropTypes.object, +} + +const withData = (Component) => { + return function WrappedComponent(props) { + const [currentAO] = useSetting(USER_DATASTORE_CURRENT_AO_KEY) + const { currentUser, orgUnitLevels } = useCachedDataQuery() + + return ( + + ) + } } -export default App +export const App = connect( + mapStateToProps, + mapDispatchToProps +)(withData(UnconnectedApp)) From b8397313b55fe4fa37913c23498aa3d81c22f1f6 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 8 Jul 2024 15:13:53 +0200 Subject: [PATCH 3/6] test: adjust tests --- .../__tests__/DimensionsPanel.spec.js | 13 +++++++++ src/components/__tests__/App.spec.js | 27 +++++-------------- src/reducers/__tests__/user.spec.js | 2 -- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js b/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js index 09ec372002..3e2876977a 100644 --- a/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js +++ b/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js @@ -3,6 +3,19 @@ import React from 'react' import { Dimensions } from '../DimensionsPanel.js' import { DndDimensionsPanel } from '../DndDimensionsPanel.js' +jest.mock('@dhis2/app-runtime', () => ({ + useDataEngine: jest.fn(() => ({ + query: Function.prototype, + })), + useCachedDataQuery: jest.fn(() => ({ + currentUser: { + settings: { + DERIVED_USER_SETTINGS_DISPLAY_NAME_PROPERTY: 'displayName', + }, + }, + })), +})) + describe('Dimensions component ', () => { let shallowDimensions let props diff --git a/src/components/__tests__/App.spec.js b/src/components/__tests__/App.spec.js index 3d801c177a..9e757688b2 100644 --- a/src/components/__tests__/App.spec.js +++ b/src/components/__tests__/App.spec.js @@ -23,6 +23,7 @@ jest.mock('../Visualization/Visualization', () => ({ describe('App', () => { let props let shallowApp + const app = () => { if (!shallowApp) { shallowApp = shallow(, { @@ -34,21 +35,11 @@ describe('App', () => { beforeEach(() => { props = { - d2: { - models: { - chart: { - get: () => { - return Promise.resolve('got a chart') - }, - }, - }, - }, baseUrl: undefined, loadError: null, interpretations: [], current: DEFAULT_CURRENT, ui: { rightSidebarOpen: false }, - location: { pathname: '/' }, settings: { rootOrganisationUnits: [ { @@ -95,10 +86,10 @@ describe('App', () => { describe('location pathname', () => { it('calls clearAll when location pathname is root', (done) => { - props.location.pathname = '/' app() setTimeout(() => { + history.push('/') expect(props.setVisualization).not.toHaveBeenCalled() expect(props.clearAll).toBeCalledTimes(1) done() @@ -106,7 +97,8 @@ describe('App', () => { }) it('calls setVisualization when location pathname has length', (done) => { - props.location.pathname = '/twilightsparkle' + history.push('/twilightsparkle') + app() setTimeout(() => { @@ -117,8 +109,6 @@ describe('App', () => { }) it('loads new visualization when pathname changes', (done) => { - props.location.pathname = '/rarity' - app() setTimeout(() => { @@ -130,8 +120,6 @@ describe('App', () => { }) it('reloads visualization when opening the same visualization', (done) => { - props.location.pathname = '/fluttershy' - app() setTimeout(() => { @@ -148,8 +136,6 @@ describe('App', () => { }) it('reloads visualization when same pathname pushed when saving', (done) => { - props.location.pathname = '/fluttershy' - app() setTimeout(() => { @@ -166,11 +152,10 @@ describe('App', () => { }) it('loads AO from user data store if id equals to "currentAnalyticalObject"', (done) => { - props.location.pathname = '/' + USER_DATASTORE_CURRENT_AO_KEY - app() setTimeout(() => { + history.push('/' + USER_DATASTORE_CURRENT_AO_KEY) expect(props.addParentGraphMap).toBeCalledTimes(1) expect(props.clearVisualization).toBeCalledTimes(1) expect(props.clearCurrent).toBeCalledTimes(1) @@ -183,7 +168,7 @@ describe('App', () => { describe('interpretation id in pathname', () => { beforeEach(() => { - props.location.pathname = `/applejack/interpretation/xyz123` + history.push('/applejack/interpretation/xyz123') }) it('does not reload visualization when interpretation toggled', (done) => { diff --git a/src/reducers/__tests__/user.spec.js b/src/reducers/__tests__/user.spec.js index af947ea67b..4fe5813827 100644 --- a/src/reducers/__tests__/user.spec.js +++ b/src/reducers/__tests__/user.spec.js @@ -29,8 +29,6 @@ describe('reducer: user', () => { const expectedState = { id, username, - uiLocale, - isSuperuser: true, } const actualState = reducer(DEFAULT_USER, action) From bf86aa06caa338012603ad9b6b38c92851aa63c5 Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 8 Jul 2024 15:14:14 +0200 Subject: [PATCH 4/6] fix: remove unused import --- src/components/Visualization/Visualization.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index a5d336db4c..f25e90ee08 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -2,7 +2,6 @@ import { DIMENSION_ID_DATA, VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_PIVOT_TABLE, - useCachedDataQuery, } from '@dhis2/analytics' import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' From 76110a5d52f54d6064734d968ce186be96f0a33c Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Mon, 8 Jul 2024 15:48:05 +0200 Subject: [PATCH 5/6] test: skip failing tests temporarily --- .../DimensionsPanel/__tests__/DimensionsPanel.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js b/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js index 3e2876977a..cca2ec7885 100644 --- a/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js +++ b/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js @@ -16,7 +16,7 @@ jest.mock('@dhis2/app-runtime', () => ({ })), })) -describe('Dimensions component ', () => { +describe.skip('Dimensions component ', () => { let shallowDimensions let props const dimensionsComponent = () => { From bdaa50db8b178646147690d7e9db10ccf3b6badb Mon Sep 17 00:00:00 2001 From: Edoardo Sabadelli Date: Tue, 9 Jul 2024 10:27:30 +0200 Subject: [PATCH 6/6] test: mock cached data provider and enable tests --- .../DimensionsPanel/__tests__/DimensionsPanel.spec.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js b/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js index cca2ec7885..4367b35e31 100644 --- a/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js +++ b/src/components/DimensionsPanel/__tests__/DimensionsPanel.spec.js @@ -7,6 +7,10 @@ jest.mock('@dhis2/app-runtime', () => ({ useDataEngine: jest.fn(() => ({ query: Function.prototype, })), +})) + +jest.mock('@dhis2/analytics', () => ({ + ...jest.requireActual('@dhis2/analytics'), useCachedDataQuery: jest.fn(() => ({ currentUser: { settings: { @@ -14,9 +18,10 @@ jest.mock('@dhis2/app-runtime', () => ({ }, }, })), + CachedDataQueryProvider: () =>
, })) -describe.skip('Dimensions component ', () => { +describe('Dimensions component ', () => { let shallowDimensions let props const dimensionsComponent = () => {