Skip to content

Commit

Permalink
WAITP-1293 Update filtering on entities route (#4688)
Browse files Browse the repository at this point in the history
* WAITP-1293 Update filtering on entities route

* WAITP-1293 No longer nest entities directly in route

* WAITP-1293: Refactor tupaia-web for flat entities data (#4692)

* fix entity menu

* refactor polygon layer

* tweaks

* Update PolygonLayer.tsx

---------

Co-authored-by: Tom Caiger <caigertom@gmail.com>
  • Loading branch information
EMcQ-BES and tcaiger committed Jul 12, 2023
1 parent 1022d43 commit 4963921
Show file tree
Hide file tree
Showing 14 changed files with 155 additions and 118 deletions.
2 changes: 1 addition & 1 deletion packages/tupaia-web-server/src/app/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function createApp() {
'requestCountryAccess',
handleWith(RequestCountryAccessRoute),
)
.get<EntitiesRequest>('entities/:hierarchyName/:rootEntityCode', handleWith(EntitiesRoute))
.get<EntitiesRequest>('entities/:projectCode/:rootEntityCode', handleWith(EntitiesRoute))
// TODO: Stop using get for logout, then delete this
.get<TempLogoutRequest>('logout', handleWith(TempLogoutRoute))
.build();
Expand Down
49 changes: 17 additions & 32 deletions packages/tupaia-web-server/src/routes/EntitiesRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,9 @@
import { Request } from 'express';
import { Route } from '@tupaia/server-boilerplate';
import camelcaseKeys from 'camelcase-keys';
import { Entity } from '@tupaia/types';
import groupBy from 'lodash.groupby';

export type EntitiesRequest = Request<any, any, any, any>;

interface NestedEntity extends Entity {
parent_code?: string | null;
children?: NestedEntity[] | null;
}

const DEFAULT_FILTER = {
generational_distance: {
comparator: '<=',
Expand All @@ -27,36 +20,28 @@ const DEFAULT_FIELDS = ['parent_code', 'code', 'name', 'type'];
export class EntitiesRoute extends Route<EntitiesRequest> {
public async buildResponse() {
const { params, query, ctx } = this.req;
const { rootEntityCode, hierarchyName } = params;
const { filter, ...restOfQuery } = query;
const { rootEntityCode, projectCode } = params;

const project = (
await ctx.services.central.fetchResources('projects', {
filter: { code: projectCode },
columns: ['entity_hierarchy.name', 'entity_hierarchy.canonical_types', 'config'],
})
)[0];
const {
'entity_hierarchy.name': hierarchyName,
// TODO: Filter entities by canonical_types and config.frontendExcluded
// 'entity_hierarchy.canonical_types': canonicalTypes,
// config: projectConfig,
} = project;

// Apply the generational_distance filter even if the user specifies other filters
// Only override it if the user specifically requests to override
const flatEntities = await ctx.services.entity.getDescendantsOfEntity(
hierarchyName,
rootEntityCode,
{ filter: { ...DEFAULT_FILTER, ...filter }, fields: DEFAULT_FIELDS, ...restOfQuery },
true,
{ filter: DEFAULT_FILTER, fields: DEFAULT_FIELDS, ...query },
query.includeRoot || false,
);

// Entity server returns a flat list of entities
// Convert that to a nested object
const nestChildrenEntities = (
entitiesByParent: Record<string, NestedEntity[]>,
parentEntity: NestedEntity,
): NestedEntity => {
const children = entitiesByParent[parentEntity.code] || [];
const nestedChildren = children.map((childEntity: NestedEntity) =>
nestChildrenEntities(entitiesByParent, childEntity),
);
// If the entity has no children, do not attach an empty array
return { ...parentEntity, children: nestedChildren.length ? nestedChildren : undefined };
};

const entitiesByParent = groupBy(flatEntities, (e: NestedEntity) => e.parent_code);
const rootEntity = flatEntities.find((entity: NestedEntity) => entity.code === rootEntityCode);
const nestedEntities = nestChildrenEntities(entitiesByParent, rootEntity);

return camelcaseKeys(nestedEntities, { deep: true });
return camelcaseKeys(flatEntities, { deep: true });
}
}
39 changes: 34 additions & 5 deletions packages/tupaia-web/src/api/queries/useEntities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,24 @@ export const useEntities = (
queryOptions?.enabled === undefined ? !!projectCode && !!entityCode : queryOptions.enabled;

return useQuery(
['entities', projectCode, entityCode, axiosConfig, queryOptions],
async (): Promise<EntityResponse> => {
return get(`entities/${projectCode}/${entityCode}`, axiosConfig);
},
['entities', projectCode, entityCode, axiosConfig],
(): Promise<EntityResponse[]> =>
get(`entities/${projectCode}/${entityCode}`, {
params: {
includeRoot: true,
fields: [
'parent_code',
'code',
'name',
'type',
'point',
'image_url',
'attributes',
'child_codes',
],
},
...axiosConfig,
}),
{
enabled,
},
Expand All @@ -29,5 +43,20 @@ export const useEntities = (

export const useEntitiesWithLocation = (projectCode?: string, entityCode?: string) =>
useEntities(projectCode, entityCode, {
params: { fields: ['parent_code', 'code', 'name', 'type', 'bounds', 'region'] },
params: {
includeRoot: true,
fields: [
'parent_code',
'code',
'name',
'type',
'bounds',
'region',
'point',
'location_type',
'image_url',
'attributes',
'child_codes',
],
},
});
48 changes: 23 additions & 25 deletions packages/tupaia-web/src/api/queries/useEntity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,32 @@
*/

import { useQuery } from 'react-query';
import { sleep } from '@tupaia/utils';

const response = {
type: 'Country',
organisationUnitCode: 'TO',
countryCode: 'TO',
name: 'Tonga',
location: {
type: 'area',
point: null,
bounds: [
[-24.1625706, 180.604802],
[-15.3655722, 186.4704542],
],
region: [],
},
photoUrl:
'https://tupaia.s3-ap-southeast-2.amazonaws.com/uploads/images/1499489588784_647646.png',
countryHierarchy: [],
};
import { useParams } from 'react-router-dom';
import { EntityResponse } from '../../types';
import { get } from '../api';
import { DEFAULT_BOUNDS } from '../../constants';

export const useEntity = (entityCode?: string) => {
// Todo: use entity endpoint when it's done and remove project code
const { projectCode } = useParams();

return useQuery(
['entity', entityCode],
async () => {
await sleep(1000);
return response;
['entities', projectCode, entityCode],
async (): Promise<EntityResponse> => {
const entities = await get(`entities/${projectCode}/${entityCode}`, {
params: {
includeRoot: true,
fields: ['parent_code', 'code', 'name', 'type', 'bounds', 'region', 'image_url'],
},
});
// @ts-ignore
const entity = entities.find(e => e.code === entityCode);

if (entity.code === 'explore') {
return { ...entity, bounds: DEFAULT_BOUNDS };
}

return entity;
},
{ enabled: !!entityCode },
);
};
14 changes: 7 additions & 7 deletions packages/tupaia-web/src/features/Dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ExpandButton } from './ExpandButton';
import { Photo } from './Photo';
import { Breadcrumbs } from './Breadcrumbs';
import { StaticMap } from './StaticMap';
import { useDashboards as useDashboardData, useEntitiesWithLocation } from '../../api/queries';
import { useDashboards as useDashboardData, useEntity } from '../../api/queries';
import { DashboardMenu } from './DashboardMenu';
import { DashboardItem } from '../DashboardItem';
import { DashboardItemType, DashboardType } from '../../types';
Expand Down Expand Up @@ -116,11 +116,11 @@ const useDashboards = () => {
};

export const Dashboard = () => {
const { projectCode, entityCode } = useParams();
const { entityCode } = useParams();
const [isExpanded, setIsExpanded] = useState(false);
const { dashboards, activeDashboard } = useDashboards();
const { data: entityData } = useEntitiesWithLocation(projectCode, entityCode);
const bounds = entityData?.bounds;
const { data: entity } = useEntity(entityCode);
const bounds = entity?.bounds;

const toggleExpanded = () => {
setIsExpanded(!isExpanded);
Expand All @@ -133,13 +133,13 @@ export const Dashboard = () => {
<Breadcrumbs />
<DashboardImageContainer>
{bounds ? (
<StaticMap polygonBounds={bounds} />
<StaticMap bounds={bounds} />
) : (
<Photo title={entityData?.name} photoUrl={entityData?.photoUrl} />
<Photo title={entity?.name} photoUrl={entity?.photoUrl} />
)}
</DashboardImageContainer>
<TitleBar>
<Title variant="h3">{entityData?.name}</Title>
<Title variant="h3">{entity?.name}</Title>
<ExportButton startIcon={<GetAppIcon />}>Export</ExportButton>
</TitleBar>
<DashboardMenu activeDashboard={activeDashboard} dashboards={dashboards} />
Expand Down
6 changes: 3 additions & 3 deletions packages/tupaia-web/src/features/Dashboard/StaticMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ const makeStaticMapUrl = (polygonBounds: Position[]) => {
return `${MAPBOX_BASE_URL}${boundingBoxPath}/${longitude},${latitude},${zoomLevel}/${size}@2x?access_token=${MAPBOX_TOKEN}&attribution=false`;
};

export const StaticMap = ({ polygonBounds }: { polygonBounds: Position[] }) => {
if (!areBoundsValid(polygonBounds)) {
export const StaticMap = ({ bounds }: { bounds: Position[] }) => {
if (!areBoundsValid(bounds)) {
return null;
}

const url = makeStaticMapUrl(polygonBounds);
const url = makeStaticMapUrl(bounds);
return <Media $backgroundImage={url} />;
};
45 changes: 20 additions & 25 deletions packages/tupaia-web/src/features/EntitySearch/EntityMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import React, { useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import styled from 'styled-components';
import { Entity } from '@tupaia/types';
import { ProjectCode } from '../../types';
import { EntityResponse, ProjectCode } from '../../types';
import { LocalHospital as HospitalIcon, ExpandMore as ExpandIcon } from '@material-ui/icons';
import { Button, IconButton, List as MuiList, ListItemProps } from '@material-ui/core';
import { useEntities } from '../../api/queries';
Expand Down Expand Up @@ -41,29 +40,27 @@ const MenuLink = styled(Button).attrs({
}
`;

type EntityWithChildren = Entity & { children?: Entity[] };

interface EntityMenuProps {
projectCode: ProjectCode;
children: EntityWithChildren[];
isLoading: boolean;
children: EntityResponse[];
grandChildren: EntityResponse[];
onClose: () => void;
}

/*
* ExpandedList is a recursive component that renders a list of entities and their children to
* display an expandable entity menu.
*/
export const EntityMenu = ({ projectCode, children, isLoading, onClose }: EntityMenuProps) => {
const entityList = children.sort((a, b) => a.name.localeCompare(b.name));
export const EntityMenu = ({ projectCode, children, grandChildren, onClose }: EntityMenuProps) => {
const sortedChildren = children.sort((a, b) => a.name.localeCompare(b.name));
return (
<List aria-expanded>
{entityList.map(entity => (
{sortedChildren.map(entity => (
<EntityMenuItem
key={entity.code}
projectCode={projectCode}
children={grandChildren.filter(child => child.parentCode === entity.code)}
entity={entity}
parentIsLoading={isLoading}
onClose={onClose}
/>
))}
Expand All @@ -73,29 +70,27 @@ export const EntityMenu = ({ projectCode, children, isLoading, onClose }: Entity

interface EntityMenuItemProps {
projectCode: ProjectCode;
entity: EntityWithChildren;
parentIsLoading?: boolean;
entity: EntityResponse;
onClose: () => void;
children?: EntityResponse[];
}
const EntityMenuItem = ({
projectCode,
entity,
parentIsLoading = false,
onClose,
}: EntityMenuItemProps) => {
const EntityMenuItem = ({ projectCode, entity, children, onClose }: EntityMenuItemProps) => {
const location = useLocation();
const [isExpanded, setIsExpanded] = useState(false);
const { data, isLoading } = useEntities(projectCode!, entity.code!, {}, { enabled: isExpanded });
const { data = [] } = useEntities(projectCode!, entity.code!, {}, { enabled: isExpanded });

const toggleExpanded = () => {
setIsExpanded(!isExpanded);
};

/*
Pre-populate the next layer of the menu with children that came from the previous layer of entity
data then replace them with the children from the API response when it arrives
*/
const nextChildren = data?.children || entity.children;
Pre-populate the next layer of the menu with children that came from the previous layer of entity
data then replace them with the children from the API response when it arrives
*/
const nextChildren =
data?.filter(childEntity => childEntity.parentCode === entity.code) || children;

const grandChildren = data.filter(childEntity => childEntity.parentCode !== entity.code);

const link = { ...location, pathname: `/${projectCode}/${entity.code}` };

Expand All @@ -107,7 +102,7 @@ const EntityMenuItem = ({
</MenuLink>
<IconButton
onClick={toggleExpanded}
disabled={parentIsLoading || !nextChildren}
disabled={!entity.childCodes}
aria-label="toggle menu for this entity"
>
<ExpandIcon />
Expand All @@ -116,9 +111,9 @@ const EntityMenuItem = ({
{isExpanded && (
<EntityMenu
children={nextChildren!}
grandChildren={grandChildren}
projectCode={projectCode}
onClose={onClose}
isLoading={isLoading}
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const isMobile = () => {
export const EntitySearch = () => {
const { projectCode } = useParams();
const { data: project } = useProject(projectCode!);
const { data: entity, isLoading } = useEntities(projectCode!, project?.entityCode);
const { data: entities = [] } = useEntities(projectCode!, project?.entityCode);
const [searchValue, setSearchValue] = useState('');
const [isOpen, setIsOpen] = useState(false);

Expand All @@ -66,6 +66,9 @@ export const EntitySearch = () => {
}
};

const children = entities.filter(entity => entity.parentCode === project?.entityCode);
const grandChildren = entities.filter(entity => entity.parentCode !== project?.entityCode);

return (
<ClickAwayListener onClickAway={() => setIsOpen(false)}>
<Wrapper>
Expand All @@ -77,8 +80,8 @@ export const EntitySearch = () => {
) : (
<EntityMenu
projectCode={projectCode!}
children={entity?.children || []}
isLoading={isLoading}
children={children}
grandChildren={grandChildren}
onClose={onClose}
/>
)}
Expand Down
Loading

0 comments on commit 4963921

Please sign in to comment.