Skip to content

Commit

Permalink
Merge pull request #42413 from tienifr/feature/empty-ui-for-create-flows
Browse files Browse the repository at this point in the history
feat: empty ui for create flows
  • Loading branch information
mountiny authored Aug 14, 2024
2 parents 3f4e2d2 + 023eba1 commit 27e62e7
Show file tree
Hide file tree
Showing 9 changed files with 809 additions and 12 deletions.
667 changes: 667 additions & 0 deletions assets/images/product-illustrations/todd-with-phones.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions src/components/EmptySelectionListContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import {View} from 'react-native';
import type {TupleToUnion} from 'type-fest';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import BlockingView from './BlockingViews/BlockingView';
import * as Illustrations from './Icon/Illustrations';
import Text from './Text';
import TextLink from './TextLink';

type EmptySelectionListContentProps = {
/** Type of selection list */
contentType: string;
};

const CONTENT_TYPES = [CONST.IOU.TYPE.SUBMIT, CONST.IOU.TYPE.SPLIT, CONST.IOU.TYPE.PAY];
type ContentType = TupleToUnion<typeof CONTENT_TYPES>;

function isContentType(contentType: unknown): contentType is ContentType {
return CONTENT_TYPES.includes(contentType as ContentType);
}

function EmptySelectionListContent({contentType}: EmptySelectionListContentProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

if (!isContentType(contentType)) {
return null;
}

const EmptySubtitle = (
<Text style={[styles.textAlignCenter]}>
{translate(`emptyList.${contentType}.subtitleText1`)}
<TextLink href={CONST.REFERRAL_PROGRAM.LEARN_MORE_LINK}>{translate(`emptyList.${contentType}.subtitleText2`)}</TextLink>
{translate(`emptyList.${contentType}.subtitleText3`)}
</Text>
);

return (
<View style={[styles.flex1, styles.overflowHidden]}>
<BlockingView
icon={Illustrations.ToddWithPhones}
iconWidth={variables.emptySelectionListIconWidth}
iconHeight={variables.emptySelectionListIconHeight}
title={translate(`emptyList.${contentType}.title`)}
shouldShowLink={false}
CustomSubtitle={EmptySubtitle}
containerStyle={[styles.mb8, styles.ph15]}
/>
</View>
);
}

EmptySelectionListContent.displayName = 'EmptySelectionListContent';

export default EmptySelectionListContent;
2 changes: 2 additions & 0 deletions src/components/Icon/Illustrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import TadaYellow from '@assets/images/product-illustrations/tada--yellow.svg';
import TeleScope from '@assets/images/product-illustrations/telescope.svg';
import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_legged_laptop_woman.svg';
import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg';
import ToddWithPhones from '@assets/images/product-illustrations/todd-with-phones.svg';
import BigVault from '@assets/images/simple-illustrations/emptystate__big-vault.svg';
import Abacus from '@assets/images/simple-illustrations/simple-illustration__abacus.svg';
import Accounting from '@assets/images/simple-illustrations/simple-illustration__accounting.svg';
Expand Down Expand Up @@ -145,6 +146,7 @@ export {
TadaYellow,
TadaBlue,
ToddBehindCloud,
ToddWithPhones,
GpsTrackOrange,
ShieldYellow,
MoneyReceipts,
Expand Down
21 changes: 14 additions & 7 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function BaseSelectionList<TItem extends ListItem>(
shouldDelayFocus = true,
shouldUpdateFocusedIndex = false,
onLongPressRow,
shouldShowListEmptyContent = false,
}: BaseSelectionListProps<TItem>,
ref: ForwardedRef<SelectionListHandle>,
) {
Expand All @@ -116,7 +117,6 @@ function BaseSelectionList<TItem extends ListItem>(
const itemFocusTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [currentPage, setCurrentPage] = useState(1);
const isTextInputFocusedRef = useRef<boolean>(false);
const isEmptyList = sections.length === 0;
const {singleExecution} = useSingleExecution();

const incrementPage = () => setCurrentPage((prev) => prev + 1);
Expand Down Expand Up @@ -475,6 +475,16 @@ function BaseSelectionList<TItem extends ListItem>(
);
};

const renderListEmptyContent = () => {
if (showLoadingPlaceholder) {
return <OptionsListSkeletonView shouldStyleAsTable={shouldUseUserSkeletonView} />;
}
if (shouldShowListEmptyContent) {
return listEmptyContent;
}
return null;
};

const scrollToFocusedIndexOnFirstRender = useCallback(
(nativeEvent: LayoutChangeEvent) => {
if (shouldUseDynamicMaxToRenderPerBatch) {
Expand Down Expand Up @@ -677,14 +687,14 @@ function BaseSelectionList<TItem extends ListItem>(
)}
{/* If we are loading new options we will avoid showing any header message. This is mostly because one of the header messages says there are no options. */}
{/* This is misleading because we might be in the process of loading fresh options from the server. */}
{(!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound')) && !!headerMessage && (
{(((!isLoadingNewOptions || headerMessage !== translate('common.noResultsFound')) && !!headerMessage) || flattenedSections.allOptions.length === 0) && (
<View style={headerMessageStyle ?? [styles.ph5, styles.pb5]}>
<Text style={[styles.textLabel, styles.colorMuted]}>{headerMessage}</Text>
</View>
)}
{!!headerContent && headerContent}
{flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? (
<OptionsListSkeletonView shouldStyleAsTable={shouldUseUserSkeletonView} />
{flattenedSections.allOptions.length === 0 ? (
renderListEmptyContent()
) : (
<>
{!listHeaderContent && header()}
Expand Down Expand Up @@ -719,9 +729,6 @@ function BaseSelectionList<TItem extends ListItem>(
style={[(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0, sectionListStyle]}
ListHeaderComponent={listHeaderContent}
ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance}
ListEmptyComponent={listEmptyContent}
contentContainerStyle={isEmptyList && listEmptyContent ? styles.flexGrow1 : undefined}
scrollEnabled={!isEmptyList || !listEmptyContent}
onEndReached={onEndReached}
onEndReachedThreshold={onEndReachedThreshold}
/>
Expand Down
5 changes: 4 additions & 1 deletion src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,7 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {
/** Custom content to display in the footer of list component. If present ShowMore button won't be displayed */
listFooterContent?: React.JSX.Element | null;

/** Content to display if the list is empty */
/** Custom content to display when the list is empty after finish loading */
listEmptyContent?: React.JSX.Element | null;

/** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */
Expand Down Expand Up @@ -489,6 +489,9 @@ type BaseSelectionListProps<TItem extends ListItem> = Partial<ChildrenProps> & {

/** Callback to fire when the item is long pressed */
onLongPressRow?: (item: TItem) => void;

/** Whether to show the empty list content */
shouldShowListEmptyContent?: boolean;
} & TRightHandSideComponent<TItem>;

type SelectionListHandle = {
Expand Down
20 changes: 20 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,26 @@ export default {
nameEmailOrPhoneNumber: 'Name, email, or phone number',
findMember: 'Find a member',
},
emptyList: {
[CONST.IOU.TYPE.SUBMIT]: {
title: 'Submit an expense',
subtitleText1: 'Submit to someone and ',
subtitleText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
subtitleText3: ' when they become a customer.',
},
[CONST.IOU.TYPE.SPLIT]: {
title: 'Split an expense',
subtitleText1: 'Split with a friend and ',
subtitleText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
subtitleText3: ' when they become a customer.',
},
[CONST.IOU.TYPE.PAY]: {
title: 'Pay someone',
subtitleText1: 'Pay anyone and ',
subtitleText2: `get $${CONST.REFERRAL_PROGRAM.REVENUE}`,
subtitleText3: ' when they become a customer.',
},
},
videoChatButtonAndMenu: {
tooltip: 'Book a call',
},
Expand Down
20 changes: 20 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,26 @@ export default {
nameEmailOrPhoneNumber: 'Nombre, email o número de teléfono',
findMember: 'Encuentra un miembro',
},
emptyList: {
[CONST.IOU.TYPE.SUBMIT]: {
title: 'Presentar un gasto',
subtitleText1: 'Presente un gasto a alguien y ',
subtitleText2: `recibe ${CONST.REFERRAL_PROGRAM.REVENUE} dólares`,
subtitleText3: ' cuando se convierta en client.',
},
[CONST.IOU.TYPE.SPLIT]: {
title: 'Dividir un gasto',
subtitleText1: 'Divide con un amigo y ',
subtitleText2: `recibe ${CONST.REFERRAL_PROGRAM.REVENUE} dólares`,
subtitleText3: ' cuando se convierta en client.',
},
[CONST.IOU.TYPE.PAY]: {
title: 'Pagar a alguien',
subtitleText1: 'Paga a quien quieras y ',
subtitleText2: `recibe ${CONST.REFERRAL_PROGRAM.REVENUE} dólares`,
subtitleText3: ' cuando se convierta en client.',
},
},
videoChatButtonAndMenu: {
tooltip: 'Programar una llamada',
},
Expand Down
25 changes: 21 additions & 4 deletions src/pages/iou/request/MoneyRequestParticipantsSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {memo, useCallback, useEffect, useMemo} from 'react';
import type {GestureResponderEvent} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Button from '@components/Button';
import EmptySelectionListContent from '@components/EmptySelectionListContent';
import FormHelpMessage from '@components/FormHelpMessage';
import {usePersonalDetails} from '@components/OnyxProvider';
import {useOptionsList} from '@components/OptionListContextProvider';
Expand Down Expand Up @@ -67,15 +68,12 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF
const {options, areOptionsInitialized} = useOptionsList({
shouldInitialize: didScreenTransitionEnd,
});

const cleanSearchTerm = useMemo(() => debouncedSearchTerm.trim().toLowerCase(), [debouncedSearchTerm]);
const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : '';

const isIOUSplit = iouType === CONST.IOU.TYPE.SPLIT;
const isCategorizeOrShareAction = [CONST.IOU.ACTION.CATEGORIZE, CONST.IOU.ACTION.SHARE].some((option) => option === action);

const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE;

useEffect(() => {
Report.searchInServer(debouncedSearchTerm.trim());
}, [debouncedSearchTerm]);
Expand Down Expand Up @@ -340,6 +338,23 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF
[shouldShowSplitBillErrorMessage, onFinish, addSingleParticipant, participants],
);

const showLoadingPlaceholder = useMemo(() => !areOptionsInitialized || !didScreenTransitionEnd, [areOptionsInitialized, didScreenTransitionEnd]);

const optionLength = useMemo(() => {
if (!areOptionsInitialized) {
return 0;
}
let length = 0;
sections.forEach((section) => {
length += section.data.length;
});
return length;
}, [areOptionsInitialized, sections]);

const shouldShowListEmptyContent = useMemo(() => optionLength === 0 && !showLoadingPlaceholder, [optionLength, showLoadingPlaceholder]);

const shouldShowReferralBanner = !isDismissed && iouType !== CONST.IOU.TYPE.INVOICE && !shouldShowListEmptyContent;

const footerContent = useMemo(() => {
if (isDismissed && !shouldShowSplitBillErrorMessage && !participants.length) {
return;
Expand Down Expand Up @@ -426,10 +441,12 @@ function MoneyRequestParticipantsSelector({participants = CONST.EMPTY_ARRAY, onF
onSelectRow={onSelectRow}
shouldSingleExecuteRowSelect
footerContent={footerContent}
listEmptyContent={<EmptySelectionListContent contentType={iouType} />}
headerMessage={header}
showLoadingPlaceholder={!areOptionsInitialized || !didScreenTransitionEnd}
showLoadingPlaceholder={showLoadingPlaceholder}
canSelectMultiple={isIOUSplit && isAllowedToSplit}
isLoadingNewOptions={!!isSearchingForReports}
shouldShowListEmptyContent={shouldShowListEmptyContent}
/>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@ export default {
borderTopWidth: 1,
emptyLHNIconWidth: 24, // iconSizeSmall + 4*2 horizontal margin
emptyLHNIconHeight: 16,
emptySelectionListIconWidth: 120,
emptySelectionListIconHeight: 125,
emptyListIconWidth: 136,
emptyListIconHeight: 144,
modalTopIconWidth: 200,
modalTopIconHeight: 164,
modalTopMediumIconHeight: 203,
modalTopBigIconHeight: 244,
modalWordmarkWidth: 154,
modalWordmarkHeight: 37,
Expand Down

0 comments on commit 27e62e7

Please sign in to comment.