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: empty ui for create flows #42413

Merged
merged 32 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
403fc65
feat: empty ui for create flows
tienifr May 21, 2024
dbbe56a
fix illustration overflow
tienifr May 22, 2024
77414dc
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr May 24, 2024
d8aab64
fix: icon is clipped in ios
tienifr May 24, 2024
bd55add
modify line break
tienifr May 24, 2024
7e2a1e7
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr May 29, 2024
b191d36
fix lint
tienifr May 29, 2024
c759252
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr May 30, 2024
f08701f
update copies and only show empty ui in some create flows
tienifr May 30, 2024
3c5a3ab
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jun 4, 2024
c7077e4
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jun 10, 2024
b06f0be
remove native ListEmptyContent
tienifr Jun 10, 2024
52d2bd9
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 3, 2024
2dad766
reapply changes
tienifr Jul 3, 2024
8dc5bd4
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 11, 2024
7534ebc
let empty state ui be obsecured by keyboard
tienifr Jul 11, 2024
9cb5622
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 15, 2024
9dbbd20
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 16, 2024
0f2106d
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 19, 2024
d616f64
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 24, 2024
2e39825
revert: let empty state ui be obsecured by keyboard
tienifr Jul 24, 2024
4a837df
resize icon to 120x125
tienifr Jul 24, 2024
554e1cb
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Jul 30, 2024
42c7e9a
fix: empty ui jumps
tienifr Aug 1, 2024
9e9a3dd
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Aug 7, 2024
ee36a72
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Aug 7, 2024
5566c28
Merge branch 'main' of https://github.com/tienifr/App into feature/em…
tienifr Aug 8, 2024
6c015dd
fix: blink ui
tienifr Aug 9, 2024
6ff0875
Merge branch 'main' into feature/empty-ui-for-create-flows
tienifr Aug 13, 2024
5f9cb38
flicker issue
tienifr Aug 13, 2024
10a792e
add padding bottom
tienifr Aug 13, 2024
023eba1
increase padding
tienifr Aug 14, 2024
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
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 @@ -672,14 +682,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()
) : (
Comment on lines +691 to 693
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line introduces #50577

More details in here: #50577 (comment)

<>
{!listHeaderContent && header()}
Expand Down Expand Up @@ -714,9 +724,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 @@ -457,6 +457,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 @@ -448,6 +448,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
Loading