Skip to content

Commit

Permalink
mobile survey select page
Browse files Browse the repository at this point in the history
  • Loading branch information
tcaiger committed Nov 7, 2024
1 parent 6e5b013 commit 2506a0f
Show file tree
Hide file tree
Showing 21 changed files with 694 additions and 308 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ const Pin = styled.img.attrs({
height: auto;
margin-right: 0.5rem;
`;
const CountrySelectWrapper = styled.div`

export const CountrySelectWrapper = styled.div`
display: flex;
align-items: center;
`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
*/

export { useUserCountries } from './useUserCountries';
export { CountrySelector } from './CountrySelector';
export { CountrySelector, CountrySelectWrapper } from './CountrySelector';
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,5 @@ export const useUserCountries = (onError?: (error: any) => void) => {
countries: alphabetisedCountries,
selectedCountry,
updateSelectedCountry: setSelectedCountry,
// if the user has a country code, and it doesn't match the selected country, then the country has been updated, which means we need to update the user
countryHasUpdated: selectedCountry?.code !== user.country?.code,
};
};
84 changes: 7 additions & 77 deletions packages/datatrak-web/src/features/GroupedSurveyList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/
import React, { ReactNode, useEffect } from 'react';
import React from 'react';
import styled from 'styled-components';
import { FormHelperText, FormLabelProps } from '@material-ui/core';
import { Country } from '@tupaia/types';
import { SelectList } from '@tupaia/ui-components';
import { SurveyFolderIcon, SurveyIcon } from '../components';
import { Survey } from '../types';
import { useCurrentUserContext, useProjectSurveys } from '../api';
import { useGroupedSurveyList } from './useGroupedSurveyList';

const ListWrapper = styled.div`
max-height: 35rem;
Expand All @@ -22,26 +21,6 @@ const ListWrapper = styled.div`
}
`;

type ListItemType = Record<string, unknown> & {
children?: ListItemType[];
content: string | ReactNode;
value: string;
selected?: boolean;
icon?: ReactNode;
tooltip?: string;
button?: boolean;
disabled?: boolean;
labelProps?: FormLabelProps & {
component?: React.ElementType;
};
};

const sortAlphanumerically = (a: ListItemType, b: ListItemType) => {
return (a.content as string).trim()?.localeCompare((b.content as string).trim(), 'en', {
numeric: true,
});
};

interface GroupedSurveyListProps {
setSelectedSurvey: (surveyCode: Survey['code'] | null) => void;
selectedSurvey: Survey['code'] | null;
Expand All @@ -61,60 +40,11 @@ export const GroupedSurveyList = ({
labelProps,
error,
}: GroupedSurveyListProps) => {
const user = useCurrentUserContext();
const { data: surveys } = useProjectSurveys(user?.projectId, selectedCountry?.code);
const groupedSurveys =
surveys
?.reduce((acc: ListItemType[], survey: Survey) => {
const { surveyGroupName, name, code } = survey;
const formattedSurvey = {
content: name,
value: code,
selected: selectedSurvey === code,
icon: <SurveyIcon />,
};
// if there is no surveyGroupName, add the survey to the list as a top level item
if (!surveyGroupName) {
return [...acc, formattedSurvey];
}
const group = acc.find(({ content }) => content === surveyGroupName);
// if the surveyGroupName doesn't exist in the list, add it as a top level item
if (!group) {
return [
...acc,
{
content: surveyGroupName,
icon: <SurveyFolderIcon />,
value: surveyGroupName,
children: [formattedSurvey],
},
];
}
// if the surveyGroupName exists in the list, add the survey to the children
return acc.map(item => {
if (item.content === surveyGroupName) {
return {
...item,
// sort the folder items alphanumerically
children: [...(item.children || []), formattedSurvey].sort(sortAlphanumerically),
};
}
return item;
});
}, [])
?.sort(sortAlphanumerically) ?? [];

useEffect(() => {
// when the surveys change, check if the selected survey is still in the list. If not, clear the selection
if (selectedSurvey && !surveys?.find(survey => survey.code === selectedSurvey)) {
setSelectedSurvey(null);
}
}, [JSON.stringify(surveys)]);

const onSelectSurvey = (listItem: ListItemType | null) => {
if (!listItem) return setSelectedSurvey(null);
setSelectedSurvey(listItem?.value as Survey['code']);
};
const { groupedSurveys, onSelectSurvey } = useGroupedSurveyList({
setSelectedSurvey,
selectedSurvey,
selectedCountry,
});
return (
<ListWrapper>
<SelectList
Expand Down
106 changes: 106 additions & 0 deletions packages/datatrak-web/src/features/MobileSelectList/ListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React, { useState, ReactNode } from 'react';
import { TransitionProps } from '@material-ui/core/transitions';
import styled from 'styled-components';
import {
Dialog,
ListItem as MuiListItem,
ListItemProps as MuiListItemProps,
Slide,
} from '@material-ui/core';
import { ArrowLeftIcon } from '../../components';
import { StickyMobileHeader } from '../../layout';
import { ListItemType } from '../useGroupedSurveyList';

const Content = styled.div`
flex: 1;
`;

const Arrow = styled(ArrowLeftIcon)`
font-size: 1rem;
color: ${({ theme }) => theme.palette.primary.main};
transform: rotate(180deg);
`;

export const BaseListItem = styled(MuiListItem)<MuiListItemProps>`
border-radius: 10px;
background: white;
padding: 1rem;
margin-bottom: 10px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
border: 1px solid transparent;
text-align: left;
`;

const IconWrapper = styled.div`
padding-right: 0.5rem;
display: flex;
align-items: center;
width: 2rem;
font-size: 2rem;
svg {
color: ${({ theme }) => theme.palette.primary.main};
height: auto;
}
`;

/**
* Taken from [Material UI's example](https://v4.mui.com/components/dialogs/#full-screen-dialogs) to make the dialog slide up from the bottom
*/
const Transition = React.forwardRef(function Transition(
props: TransitionProps & { children?: React.ReactElement },
ref: React.Ref<unknown>,
) {
return <Slide direction="left" ref={ref} {...props} />;
});

interface ListItemProps {
item: ListItemType;
onSelect: (item: any) => void;
children?: ReactNode;
}

export const ListItem = ({ item, onSelect, children }: ListItemProps) => {
const [expanded, setExpanded] = useState(false);
const { content, icon } = item;
const isNested = !!item.children;

const onClose = () => setExpanded(false);

const handleOnClick = () => {
if (children) {
setExpanded(true);
} else {
onSelect(item);
}
};

return (
<>
<BaseListItem button onClick={handleOnClick}>
<IconWrapper>{icon}</IconWrapper>
<Content>{content}</Content>
{isNested && <Arrow />}
</BaseListItem>
{children && (
<Dialog
open={expanded}
TransitionComponent={Transition}
keepMounted={false}
onClose={onClose}
fullScreen
>
<StickyMobileHeader onBack={onClose} onClose={onClose} title="Select a survey" />
{children}
</Dialog>
)}
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

import React from 'react';
import styled from 'styled-components';
import { List as MuiList, Typography } from '@material-ui/core';
import { ListItem } from './ListItem';
import { CountrySelectWrapper } from '../CountrySelector';
import { ListItemType } from '../useGroupedSurveyList';

const BaseList = styled(MuiList)`
padding: 20px 25px;
height: 100%;
background: ${({ theme }) => theme.palette.background.default};
${CountrySelectWrapper} {
margin-bottom: 1rem;
.MuiOutlinedInput-notchedOutline {
border: none;
}
.MuiInputBase-root .MuiSvgIcon-root {
display: none;
}
}
`;

const CategoryTitle = styled(Typography)`
margin: -0.5rem 0 0.8rem;
padding-top: 1rem;
border-top: 1px solid ${({ theme }) => theme.palette.divider};
`;

const NoResultsMessage = styled(Typography)`
padding: 0.8rem 0.5rem;
font-size: 0.875rem;
color: ${({ theme }) => theme.palette.text.secondary};
`;

interface SelectListProps {
items?: ListItemType[];
onSelect: (item: ListItemType) => void;
CountrySelector: React.ReactNode;
}

const List = ({ parentItem, items, onSelect, CountrySelector }) => {
const parentTitle = parentItem?.value;
return (
<>
<BaseList>
{CountrySelector}
{parentTitle && <CategoryTitle>{parentTitle}</CategoryTitle>}
{items?.map(item => (
<ListItem item={item} onSelect={onSelect} key={item.value}>
{item?.children && (
<List
parentItem={item}
items={item.children}
onSelect={onSelect}
CountrySelector={CountrySelector}
/>
)}
</ListItem>
))}
</BaseList>
</>
);
};

export const MobileSelectList = ({ items = [], onSelect, CountrySelector }: SelectListProps) => {
return (
<>
{items.length === 0 ? (
<NoResultsMessage>No items to display</NoResultsMessage>
) : (
<BaseList>
{CountrySelector}
{items?.map(item => (
<ListItem item={item} onSelect={onSelect} key={item.value}>
{item?.children && (
<List
parentItem={item}
items={item.children}
onSelect={onSelect}
CountrySelector={CountrySelector}
/>
)}
</ListItem>
))}
</BaseList>
)}
</>
);
};
6 changes: 6 additions & 0 deletions packages/datatrak-web/src/features/MobileSelectList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Tupaia
* Copyright (c) 2017 - 2024 Beyond Essential Systems Pty Ltd
*/

export { MobileSelectList } from './MobileSelectList';
6 changes: 4 additions & 2 deletions packages/datatrak-web/src/features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,20 @@ export {
SurveyReviewScreen,
SurveyContext,
SurveyLayout,
SurveyToolbar,
useSurveyForm,
getAllSurveyComponents,
SurveySideMenu,
useValidationResolver,
SurveyResubmitSuccessScreen,
SurveyToolbar,
} from './Survey';
export { RequestProjectAccess } from './RequestProjectAccess';
export { MobileAppPrompt } from './MobileAppPrompt';
export { Leaderboard } from './Leaderboard';
export { Reports } from './Reports';
export { TaskPageHeader, TasksTable, TaskDetails, CreateTaskModal, TaskActionsMenu } from './Tasks';
export { useUserCountries, CountrySelector } from './CountrySelector';
export { useUserCountries, CountrySelector, CountrySelectWrapper } from './CountrySelector';
export { GroupedSurveyList } from './GroupedSurveyList';
export { useGroupedSurveyList } from './useGroupedSurveyList';
export { SurveyResponseModal } from './SurveyResponseModal';
export { MobileSelectList } from './MobileSelectList';
Loading

0 comments on commit 2506a0f

Please sign in to comment.