Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tupaiaWeb): RN-1111: Disable map overlays for levels higher than current entity #5888

Merged
merged 12 commits into from
Oct 23, 2024
21 changes: 21 additions & 0 deletions packages/tupaia-web-server/src/routes/MapOverlaysRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import {
Entity,
MapOverlay,
MapOverlayGroup,
MapOverlayGroupRelation,
Expand Down Expand Up @@ -77,6 +78,20 @@ export class MapOverlaysRoute extends Route<MapOverlaysRequest> {
const { pageSize } = query;

const entity = await ctx.services.entity.getEntity(projectCode, entityCode);

const ancestors: Entity[] = await ctx.services.entity.getAncestorsOfEntity(
projectCode,
entityCode,
{
fields: ['code', 'type'],
},
);

// get the types of the ancestors, excluding the current entity
const ancestorTypes = ancestors
.filter(ancestor => ancestor.code !== entityCode)
.map(ancestor => ancestor.type.toLowerCase().replace('_', ''));

const rootEntityCode = entity.country_code || entity.code;

// Do the initial overlay fetch from the central server, since that enforces permissions
Expand Down Expand Up @@ -141,6 +156,11 @@ export class MapOverlaysRoute extends Route<MapOverlaysRequest> {
(relation: MapOverlayGroupRelation) => {
if (relation.child_type === MAP_OVERLAY_CHILD_TYPE) {
const overlay = overlaysById[relation.child_id];

// If the measure level is found in the ancestor types, that means the currently selected entity is a descendant of the measure level entity, so there will be no data to display. In this case, the overlay should be disabled.
const isDisabled = overlay.config.measureLevel
tcaiger marked this conversation as resolved.
Show resolved Hide resolved
? ancestorTypes.includes(overlay.config.measureLevel.toLowerCase())
: false;
// Translate Map Overlay
return {
name: overlay.name,
Expand All @@ -149,6 +169,7 @@ export class MapOverlaysRoute extends Route<MapOverlaysRequest> {
legacy: overlay.legacy,
sortOrder: relation.sort_order,
entityAttributesFilter: overlay.entity_attributes_filter,
disabled: isDisabled,
...overlay.config,
} as TranslatedMapOverlay;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ import { Entity } from '../../../types';
import { useExportMapOverlay } from '../../../api/mutations';
import { useEntity, useMapOverlays, useProject, useUser } from '../../../api/queries';
import { MOBILE_BREAKPOINT, URL_SEARCH_PARAMS } from '../../../constants';
import { convertDateRangeToUrlPeriodString, useDateRanges, useGAEffect } from '../../../utils';
import {
convertDateRangeToUrlPeriodString,
getFriendlyEntityType,
useDateRanges,
useGAEffect,
} from '../../../utils';
import { MapTableModal } from './MapTableModal';
import { MapOverlayList } from './MapOverlayList';
import { MapOverlayDatePicker } from './MapOverlayDatePicker';
Expand Down Expand Up @@ -271,12 +276,14 @@ export const DesktopMapOverlaySelector = ({

const exportTooltip = getExportTooltip();

const friendlyEntityType = getFriendlyEntityType(entity?.type);

return (
<>
{mapModalOpen && <MapTableModal onClose={toggleMapTableModal} />}
<Wrapper>
<Header>
<Heading>Map Overlays</Heading>
<Heading>Map Overlays {friendlyEntityType && `(${friendlyEntityType})`}</Heading>
{selectedOverlay && (
<div>
{isLoggedIn && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import React, { ChangeEvent, useEffect, useState } from 'react';
import { useParams } from 'react-router';
import { useSearchParams } from 'react-router-dom';
import styled from 'styled-components';
import { ICON_STYLES, ReferenceTooltip } from '@tupaia/ui-components';
import { ICON_STYLES, ReferenceTooltip, Tooltip } from '@tupaia/ui-components';
import {
Accordion,
AccordionDetails,
Expand All @@ -16,10 +16,11 @@ import {
Radio,
RadioGroup,
} from '@material-ui/core';
import { TupaiaWebMapOverlaysRequest } from '@tupaia/types';
import { EntityType, TupaiaWebMapOverlaysRequest } from '@tupaia/types';
import { KeyboardArrowRight } from '@material-ui/icons';
import { useMapOverlays } from '../../../api/queries';
import { useEntity, useMapOverlays } from '../../../api/queries';
import { DEFAULT_PERIOD_PARAM_STRING, URL_SEARCH_PARAMS } from '../../../constants';
import { getFriendlyEntityType } from '../../../utils';

const AccordionWrapper = styled(Accordion)`
background-color: transparent;
Expand Down Expand Up @@ -61,7 +62,7 @@ const AccordionHeader = styled(AccordionSummary)`
}
`;

const Wrapper = styled.div`
const RadioItemWrapper = styled.div`
display: flex;
align-items: center;
`;
Expand Down Expand Up @@ -90,18 +91,29 @@ const AccordionContent = styled(AccordionDetails)`
const FormLabel = styled(FormControlLabel)`
border-radius: 3px;

&.Mui-disabled .MuiSvgIcon-root {
color: rgba(255, 255, 255, 0.5);
}
&:hover {
background: rgba(153, 153, 153, 0.2);
}
`;
const MapOverlayItemWrapper = ({ disabled, entityType, children }) => {
if (!disabled) {
return children;
}
return <Tooltip title={`Not available at ${entityType} level`}>{children}</Tooltip>;
};

/**
* A recursive component that renders a list of map overlays in an accordion.
*/
const MapOverlayAccordion = ({
mapOverlayGroup,
mapOverlayListItem,
entityType,
}: {
mapOverlayGroup: TupaiaWebMapOverlaysRequest.TranslatedMapOverlayGroup;
mapOverlayListItem: TupaiaWebMapOverlaysRequest.TranslatedMapOverlayGroup;
entityType?: EntityType;
}) => {
const [expanded, setExpanded] = useState(false);
const toggleExpanded = () => {
Expand All @@ -111,32 +123,39 @@ const MapOverlayAccordion = ({
return (
<AccordionWrapper expanded={expanded} onChange={toggleExpanded} square>
<AccordionHeader expandIcon={<KeyboardArrowRight />}>
{mapOverlayGroup.name}
{mapOverlayGroup.info?.reference && (
{mapOverlayListItem.name}
{mapOverlayListItem.info?.reference && (
<ReferenceTooltip
reference={mapOverlayGroup.info.reference}
reference={mapOverlayListItem.info.reference}
iconStyle={ICON_STYLES.MAP_OVERLAY}
/>
)}
</AccordionHeader>
<AccordionContent>
{/* Map through the children, and if there are more nested children, render another
accordion. Otherwise, render radio input for the overlay */}
{mapOverlayGroup.children.map(mapOverlay =>
'children' in mapOverlay ? (
<MapOverlayAccordion mapOverlayGroup={mapOverlay} key={mapOverlay.name} />
{mapOverlayListItem.children.map(child =>
'children' in child ? (
<MapOverlayAccordion
mapOverlayListItem={child}
key={child.name}
entityType={entityType}
/>
) : (
<Wrapper>
<FormLabel
value={mapOverlay.code}
control={<Radio />}
label={mapOverlay.name}
key={mapOverlay.code}
/>
{mapOverlay.info?.reference && (
<ReferenceTooltip reference={mapOverlay.info.reference} iconStyle="mapOverlay" />
)}
</Wrapper>
<MapOverlayItemWrapper disabled={child.disabled} entityType={entityType}>
<RadioItemWrapper>
<FormLabel
value={child.code}
control={<Radio />}
label={child.name}
key={child.code}
disabled={child.disabled}
/>
{child.info?.reference && (
<ReferenceTooltip reference={child.info.reference} iconStyle="mapOverlay" />
)}
</RadioItemWrapper>
</MapOverlayItemWrapper>
),
)}
</AccordionContent>
Expand Down Expand Up @@ -196,6 +215,7 @@ const useSavedMapOverlayDates = () => {
export const MapOverlayList = ({ toggleOverlayLibrary }: { toggleOverlayLibrary?: () => void }) => {
const [urlSearchParams, setUrlParams] = useSearchParams();
const { projectCode, entityCode } = useParams();
const { data: entity } = useEntity(projectCode, entityCode, true);
const { getSavedMapOverlayDateRange } = useSavedMapOverlayDates();
const {
mapOverlayGroups = [],
Expand All @@ -219,6 +239,7 @@ export const MapOverlayList = ({ toggleOverlayLibrary }: { toggleOverlayLibrary?

if (isLoadingMapOverlays) return null;

const friendlyEntityType = getFriendlyEntityType(entity?.type);
tcaiger marked this conversation as resolved.
Show resolved Hide resolved
return (
<RadioGroupContainer
aria-label="Map overlays"
Expand All @@ -229,7 +250,11 @@ export const MapOverlayList = ({ toggleOverlayLibrary }: { toggleOverlayLibrary?
{mapOverlayGroups
.filter(item => item.name)
.map(group => (
<MapOverlayAccordion mapOverlayGroup={group} key={group.name} />
<MapOverlayAccordion
mapOverlayListItem={group}
key={group.name}
entityType={friendlyEntityType}
/>
))}
</RadioGroupContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { Typography } from '@material-ui/core';
import { ArrowBack, ArrowForwardIos } from '@material-ui/icons';
import { Button } from '@tupaia/ui-components';
import { MOBILE_BREAKPOINT } from '../../../constants';
import { getMobileTopBarHeight } from '../../../utils';
import { useMapOverlays } from '../../../api/queries';
import { getFriendlyEntityType, getMobileTopBarHeight } from '../../../utils';
import { useEntity, useMapOverlays } from '../../../api/queries';
import { MapOverlayList } from './MapOverlayList';
import { MapOverlaySelectorTitle } from './MapOverlaySelectorTitle';
import { MapOverlayDatePicker } from './MapOverlayDatePicker';
Expand Down Expand Up @@ -113,8 +113,11 @@ export const MobileMapOverlaySelector = ({
toggleOverlayLibrary,
}: MobileMapOverlaySelectorProps) => {
const { projectCode, entityCode } = useParams();
const { data: entity } = useEntity(projectCode, entityCode, true);
const { hasMapOverlays } = useMapOverlays(projectCode, entityCode);

const friendlyEntityType = getFriendlyEntityType(entity?.type);

return (
<Wrapper>
<Container>
Expand Down Expand Up @@ -146,7 +149,9 @@ export const MobileMapOverlaySelector = ({
<ArrowWrapper>
<ArrowBack />
</ArrowWrapper>
<OverlayLibraryHeader>Overlay Library</OverlayLibraryHeader>
<OverlayLibraryHeader>
Overlay Library {friendlyEntityType && `(${friendlyEntityType})`}
</OverlayLibraryHeader>
</OverlayLibraryHeaderButton>
<OverlayListWrapper>
{/* Use the entity code as a key so that the local state of the MapOverlayList resets between entities */}
Expand Down
24 changes: 18 additions & 6 deletions packages/tupaia-web/src/features/Map/utils/useDefaultMapOverlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom';
import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom';
import { useMapOverlays, useProject } from '../../../api/queries';
import {
DEFAULT_MAP_OVERLAY_ID,
Expand All @@ -18,6 +18,7 @@ import { EntityCode, ProjectCode } from '../../../types';
export const useDefaultMapOverlay = (projectCode?: ProjectCode, entityCode?: EntityCode) => {
const queryClient = useQueryClient();
const navigate = useNavigate();
const { dashboardName } = useParams();
const location = useLocation();
const [urlSearchParams] = useSearchParams();
const { data: project } = useProject(projectCode);
Expand All @@ -27,7 +28,8 @@ export const useDefaultMapOverlay = (projectCode?: ProjectCode, entityCode?: Ent
const selectedMapOverlay = urlSearchParams.get(URL_SEARCH_PARAMS.MAP_OVERLAY);
const selectedMapOverlayPeriod = urlSearchParams.get(URL_SEARCH_PARAMS.MAP_OVERLAY_PERIOD);

const isValidMapOverlayId = !!mapOverlaysByCode[selectedMapOverlay!];
const isValidMapOverlayId =
!!mapOverlaysByCode[selectedMapOverlay!] && !mapOverlaysByCode[selectedMapOverlay!].disabled;
const overlayCodes = mapOverlaysByCode ? Object.keys(mapOverlaysByCode) : [];

const getDefaultOverlayCode = () => {
Expand All @@ -36,18 +38,26 @@ export const useDefaultMapOverlay = (projectCode?: ProjectCode, entityCode?: Ent
const { defaultMeasure } = project;

// if the defaultMeasure exists, use this
if (mapOverlaysByCode[defaultMeasure as string]) {
if (
defaultMeasure &&
mapOverlaysByCode[defaultMeasure] &&
!mapOverlaysByCode[defaultMeasure].disabled
) {
return defaultMeasure;
}

// if the generic default overlay exists, use this
if (mapOverlaysByCode[DEFAULT_MAP_OVERLAY_ID]) {
if (
mapOverlaysByCode[DEFAULT_MAP_OVERLAY_ID] &&
!mapOverlaysByCode[DEFAULT_MAP_OVERLAY_ID].disabled
) {
return DEFAULT_MAP_OVERLAY_ID;
}

// otherwise use the first overlay in the list
if (allMapOverlays.length > 0) {
return allMapOverlays[0].code;
const firstNonDisabledOverlay = allMapOverlays.find(overlay => !overlay.disabled);
return firstNonDisabledOverlay?.code;
}
}
};
Expand All @@ -62,6 +72,7 @@ export const useDefaultMapOverlay = (projectCode?: ProjectCode, entityCode?: Ent
}

const defaultOverlayCode = getDefaultOverlayCode();

if (defaultOverlayCode) {
urlSearchParams.set(URL_SEARCH_PARAMS.MAP_OVERLAY, defaultOverlayCode as string);
}
Expand All @@ -74,5 +85,6 @@ export const useDefaultMapOverlay = (projectCode?: ProjectCode, entityCode?: Ent
...location,
search: urlSearchParams.toString(),
});
}, [JSON.stringify(mapOverlaysByCode), project, selectedMapOverlay]);
}, [JSON.stringify(mapOverlaysByCode), project, selectedMapOverlay, dashboardName]);
// include dashboardName in the dependencies to ensure that the default overlay is updated when the dashboard changes
};
13 changes: 13 additions & 0 deletions packages/tupaia-web/src/utils/getFriendlyEntityType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

/**
* @description Entity types are stored as strings with '_' separating words. This function converts it to a human-readable format.
*/
export const getFriendlyEntityType = entityType => {
tcaiger marked this conversation as resolved.
Show resolved Hide resolved
if (!entityType) return '';

return entityType.toLowerCase().replace('_', '-');
};
1 change: 1 addition & 0 deletions packages/tupaia-web/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ export { useGAEffect } from './useGAEffect';
export { useUrlLoginToken } from './useUrlLoginToken';
export { getTopBarHeight, getMobileTopBarHeight } from './getTopBarHeight';
export { downloadPDF } from './downloadPDF';
export { getFriendlyEntityType } from './getFriendlyEntityType';
Loading