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: Add mobile app activity group empty states (M2-8098) #905

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion assets/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
"common_refresh_error": "The applets list was not refreshed. Please try again."
},
"activity_list_component": {
"no_activities_yet": "No activities specified yet",
"no_activities": "No activities are available for you to complete right now",
"insufficient_data_error": "This applet was not refreshed. Please try to refresh again.",
"other_error": "Undefined error occurred"
},
Expand Down
2 changes: 1 addition & 1 deletion assets/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@
"common_refresh_error": "La liste des applets n'a pas Γ©tΓ© actualisΓ©e. Veuillez rΓ©essayer."
},
"activity_list_component": {
"no_activities_yet": "Aucune activitΓ© spΓ©cifiΓ©e pour le moment",
"no_activities": "Aucune activitΓ© n'est disponible pour le moment",
"insufficient_data_error": "Cette applet n'a pas Γ©tΓ© actualisΓ©e. Veuillez rΓ©essayer d'actualiser.",
"other_error": "Une erreur non dΓ©finie s'est produite"
},
Expand Down
10 changes: 10 additions & 0 deletions src/shared/ui/icons/ChecklistIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Svg, { Path, SvgProps } from 'react-native-svg';

export const ChecklistIcon = (props: SvgProps) => (
<Svg width={80} height={80} viewBox="0 0 80 80" fill="#72777F" {...props}>
<Path d="M40.314 13.5894C40.7202 13.7586 41.0889 14.0066 41.3987 14.319C41.7371 14.6311 42.0071 15.01 42.1918 15.4316C42.3765 15.8533 42.4719 16.3087 42.4719 16.769C42.4719 17.2293 42.3765 17.6847 42.1918 18.1064C42.0071 18.528 41.7371 18.9069 41.3987 19.219L23.5719 37.6384C23.262 37.9508 22.8933 38.1988 22.4871 38.368C22.0809 38.5372 21.6452 38.6244 21.2052 38.6244C20.7652 38.6244 20.3295 38.5372 19.9233 38.368C19.5171 38.1988 19.1484 37.9508 18.8385 37.6384L7.70551 26.6663C7.38377 26.3555 7.1264 25.9844 6.94809 25.5742C6.76978 25.1639 6.67402 24.7225 6.66628 24.2753C6.65854 23.828 6.73898 23.3836 6.90299 22.9674C7.067 22.5512 7.31138 22.1714 7.62218 21.8497C7.93297 21.5279 8.3041 21.2706 8.71436 21.0923C9.12462 20.9139 9.56598 20.8182 10.0133 20.8104C10.4605 20.8027 10.9049 20.8831 11.3211 21.0472C11.7373 21.2112 12.1171 21.4555 12.4388 21.7663L21.2052 30.405L36.6654 14.319C36.9753 14.0066 37.344 13.7586 37.7502 13.5894C38.1564 13.4201 38.592 13.333 39.0321 13.333C39.4721 13.333 39.9078 13.4201 40.314 13.5894Z" />
<Path d="M40.314 43.5894C40.7202 43.7586 41.0889 44.0066 41.3987 44.319C41.7371 44.6311 42.0071 45.01 42.1918 45.4316C42.3765 45.8533 42.4719 46.3087 42.4719 46.769C42.4719 47.2293 42.3765 47.6847 42.1918 48.1064C42.0071 48.528 41.7371 48.9069 41.3987 49.219L23.5719 67.6384C23.262 67.9508 22.8933 68.1988 22.4871 68.368C22.0809 68.5372 21.6452 68.6244 21.2052 68.6244C20.7652 68.6244 20.3295 68.5372 19.9233 68.368C19.5171 68.1988 19.1484 67.9508 18.8385 67.6384L7.70551 56.6663C7.38377 56.3555 7.1264 55.9844 6.94809 55.5742C6.76978 55.1639 6.67402 54.7225 6.66628 54.2753C6.65854 53.828 6.73898 53.3836 6.90299 52.9674C7.067 52.5512 7.31138 52.1714 7.62218 51.8497C7.93297 51.5279 8.3041 51.2706 8.71436 51.0922C9.12462 50.9139 9.56598 50.8182 10.0133 50.8104C10.4605 50.8027 10.9049 50.8831 11.3211 51.0471C11.7373 51.2112 12.1171 51.4555 12.4388 51.7663L21.2052 60.405L36.6654 44.319C36.9753 44.0066 37.344 43.7586 37.7502 43.5894C38.1564 43.4201 38.592 43.333 39.0321 43.333C39.4721 43.333 39.9078 43.4201 40.314 43.5894Z" />
<Path d="M46.6663 26.6663C46.6663 24.8254 48.1587 23.333 49.9996 23.333H69.9996C71.8406 23.333 73.3329 24.8254 73.3329 26.6663C73.3329 28.5073 71.8406 29.9997 69.9996 29.9997H49.9996C48.1587 29.9997 46.6663 28.5073 46.6663 26.6663Z" />
<Path d="M49.9996 49.9997C48.1587 49.9997 46.6663 51.4921 46.6663 53.333C46.6663 55.174 48.1587 56.6663 49.9996 56.6663H69.9996C71.8406 56.6663 73.3329 55.174 73.3329 53.333C73.3329 51.4921 71.8406 49.9997 69.9996 49.9997H49.9996Z" />
</Svg>
);
65 changes: 43 additions & 22 deletions src/widgets/activity-group/ui/ActivityGroups.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FC } from 'react';
import { FC, useMemo } from 'react';

import { useIsFetching } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';

import {
CheckAvailability,
Expand All @@ -9,10 +10,12 @@ import {
import { getAppletCompletedEntitiesKey } from '@app/shared/lib/utils/reactQueryHelpers';
import { ActivityIndicator } from '@app/shared/ui/ActivityIndicator';
import { Box, BoxProps, XStack, YStack } from '@app/shared/ui/base';
import { ChecklistIcon } from '@app/shared/ui/icons/ChecklistIcon';
import { LoadListError } from '@app/shared/ui/LoadListError';
import { NoListItemsYet } from '@app/shared/ui/NoListItemsYet';

import { ActivitySectionList } from './ActivitySectionList';
import { EmptyState } from './EmptyState';
import { ActivityGroupType } from '../lib/types/activityGroup';
import { useActivityGroups } from '../model/hooks/useActivityGroups';
import { useBaseInfo } from '../model/hooks/useBaseInfo';

Expand All @@ -23,6 +26,8 @@ type Props = {
} & BoxProps;

export const ActivityGroups: FC<Props> = props => {
const { t } = useTranslation();

const isLoadingCompletedEntities =
useIsFetching({
exact: true,
Expand All @@ -34,6 +39,26 @@ export const ActivityGroups: FC<Props> = props => {
const { responseTypes } = data || {};
const hasError = !isSuccess || !!baseInfoError;

const renderedGroups = useMemo(() => {
const hasActivities = groups.some(g => g.activities.length);

if (hasActivities) {
// Only show empty available group if there are no in-progress activities
const showAvailableGroup = !groups.some(
g => g.type === ActivityGroupType.InProgress && g.activities.length,
);

// Filter out empty groups, but show the available group based on above logic
return groups.filter(
g =>
g.activities.length ||
(g.type === ActivityGroupType.Available && showAvailableGroup),
);
} else {
return null;
}
}, [groups]);

if (isLoadingCompletedEntities || isLoading) {
return (
<Box
Expand Down Expand Up @@ -63,29 +88,25 @@ export const ActivityGroups: FC<Props> = props => {
);
}

if (isSuccess && !groups?.length) {
return (
<XStack
accessibilityLabel="activity-group-empty"
flex={1}
jc="center"
ai="center"
>
<NoListItemsYet translationKey="activity_list_component:no_activities_yet" />
</XStack>
);
}

return (
<Box {...props}>
<YStack accessibilityLabel="activity-group-list" flex={1}>
<ActivitySectionList
activityResponseTypes={responseTypes}
appletId={props.appletId}
groups={groups}
completeEntity={props.completeEntity}
checkAvailability={props.checkAvailability}
/>
{renderedGroups ? (
<ActivitySectionList
activityResponseTypes={responseTypes}
appletId={props.appletId}
groups={renderedGroups}
completeEntity={props.completeEntity}
checkAvailability={props.checkAvailability}
/>
) : (
<EmptyState
accessibilityLabel="activity-group-empty"
flex={1}
icon={<ChecklistIcon />}
description={t('activity_list_component:no_activities')}
/>
)}
</YStack>
</Box>
);
Expand Down
26 changes: 16 additions & 10 deletions src/widgets/activity-group/ui/ActivitySectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ import {
getSupportsMobile,
} from '@app/shared/lib/utils/responseTypes';
import { Box, YStack } from '@app/shared/ui/base';
import { ChecklistIcon } from '@app/shared/ui/icons/ChecklistIcon';
import { Text } from '@app/shared/ui/Text';

import { EmptyState } from './EmptyState';
import { ActivityListGroup } from '../lib/types/activityGroup';
import { useAvailabilityEvaluator } from '../model/hooks/useAvailabilityEvaluator';

Expand All @@ -54,16 +56,10 @@ export function ActivitySectionList({

const { isUploading } = useUploadObservable();

const sections = useMemo(() => {
return groups
.filter(g => g.activities.length)
.map(group => {
return {
data: group.activities,
key: t(group.name),
};
});
}, [t, groups]);
const sections = useMemo(
() => groups.map(group => ({ data: group.activities, key: t(group.name) })),
[t, groups],
);

const { startFlow, startActivity } = useStartEntity({
hasMediaReferences: getDefaultMediaLookupService().hasMediaReferences,
Expand Down Expand Up @@ -200,6 +196,16 @@ export function ActivitySectionList({
/>
);
}}
// SectionList doesn't provide a prop for section empty components, so we use
// renderSectionFooter to conditionally render any empty sections.
renderSectionFooter={({ section }) =>
section.data.length ? null : (
<EmptyState
icon={<ChecklistIcon />}
description={t('activity_list_component:no_activities')}
/>
)
}
ItemSeparatorComponent={ItemSeparator}
stickySectionHeadersEnabled={false}
contentContainerStyle={styles.sectionList}
Expand Down
26 changes: 26 additions & 0 deletions src/widgets/activity-group/ui/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ReactNode } from 'react';

import { BoxProps, YStack } from '@app/shared/ui/base';
import { Text } from '@app/shared/ui/Text';

type Props = BoxProps & {
icon: ReactNode;
description: string;
};

export const EmptyState = ({ icon, description, ...rest }: Props) => {
return (
<YStack
alignItems="center"
justifyContent="center"
py={16}
gap={16}
{...rest}
>
{icon}
<Text fontSize={24} lineHeight={32} color="$grey4" textAlign="center">
{description}
</Text>
</YStack>
);
};
Loading