From 3f019096d3186ac938f383549eb35ca06474ba21 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 6 May 2024 18:35:27 +0700 Subject: [PATCH 01/44] fix: tapping assignee and mark as complete quickly navigates to not found page --- src/components/MenuItem.tsx | 10 +- src/components/TaskHeaderActionButton.tsx | 15 ++- src/pages/home/ReportScreen.tsx | 147 +++++++++++---------- src/pages/settings/Profile/ProfilePage.tsx | 7 +- 4 files changed, 95 insertions(+), 84 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index bf35d65340fc..10d2b72a0ab9 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -342,7 +342,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [style, styles.popoverMenuItem]; const {isSmallScreenWidth} = useWindowDimensions(); - const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; + const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -417,15 +417,11 @@ function MenuItem( } if (onPress && event) { - if (!singleExecution || !waitForNavigate) { + if (!singleExecution) { onPress(event); return; } - singleExecution( - waitForNavigate(() => { - onPress(event); - }), - )(); + singleExecution(onPress)(event); } }; diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 788734242f7b..b9aaf562b471 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useContext} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -10,6 +10,7 @@ import * as Task from '@userActions/Task'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; +import {MenuItemGroupContext} from './MenuItemGroup'; type TaskHeaderActionButtonOnyxProps = { /** Current user session */ @@ -24,6 +25,16 @@ type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & { function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const {singleExecution} = useContext(MenuItemGroupContext) ?? {}; + + const onPressAction = () => { + const onPress = () => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)); + if (!singleExecution) { + onPress(); + return; + } + singleExecution(onPress)(); + }; if (!ReportUtils.canWriteInReport(report)) { return null; @@ -36,7 +47,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? 0)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} + onPress={Session.checkIfActionIsAllowed(onPressAction)} style={styles.flex1} /> diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index b82137756a28..743d32c38009 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -12,6 +12,7 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import * as Illustrations from '@components/Icon/Illustrations'; +import MenuItemGroup from '@components/MenuItemGroup'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -651,85 +652,87 @@ function ReportScreen({ return ( - - + - - {headerView} - {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( - - - - + + {headerView} + {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + + + + + - - )} - - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( - - )} - - - {shouldShowReportActionList && ( - )} - - {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. + + {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {shouldShowReportActionList && ( + + )} + + {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. If we prevent rendering the report while they are loading then we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {shouldShowSkeleton && } - - {isCurrentReportLoadedFromOnyx ? ( - setIsComposerFocus(true)} - onComposerBlur={() => setIsComposerFocus(false)} - report={report} - pendingAction={reportPendingAction} - isComposerFullSize={!!isComposerFullSize} - listHeight={listHeight} - isEmptyChat={isEmptyChat} - lastReportAction={lastReportAction} - /> - ) : null} - - - - + {shouldShowSkeleton && } + + {isCurrentReportLoadedFromOnyx ? ( + setIsComposerFocus(true)} + onComposerBlur={() => setIsComposerFocus(false)} + report={report} + pendingAction={reportPendingAction} + isComposerFullSize={!!isComposerFullSize} + listHeight={listHeight} + isEmptyChat={isEmptyChat} + lastReportAction={lastReportAction} + /> + ) : null} + + + + + ); diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 4c5ed88e6898..f33a86ed2d46 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,11 +1,11 @@ -import React, {useEffect} from 'react'; +import React, {useContext, useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; -import MenuItemGroup from '@components/MenuItemGroup'; +import MenuItemGroup, {MenuItemGroupContext} from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -61,6 +61,7 @@ function ProfilePage({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); + const {waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const getPronouns = (): string => { const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; @@ -179,7 +180,7 @@ function ProfilePage({ title={detail.title} description={detail.description} wrapperStyle={styles.sectionMenuItemTopDescription} - onPress={() => Navigation.navigate(detail.pageRoute)} + onPress={waitForNavigate ? waitForNavigate(() => Navigation.navigate(detail.pageRoute)) : () => Navigation.navigate(detail.pageRoute)} /> ))} From 9b6b6994df575312e9fd736c560e5c871029aaeb Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 19 Jun 2024 17:11:24 +0700 Subject: [PATCH 02/44] revert singleExecution approach --- src/components/TaskHeaderActionButton.tsx | 15 +- src/pages/home/ReportScreen.tsx | 161 ++++++++++----------- src/pages/settings/Profile/ProfilePage.tsx | 104 +++++++------ 3 files changed, 131 insertions(+), 149 deletions(-) diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index c56bc6d9704a..0c7e603a4aa2 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -1,4 +1,4 @@ -import React, {useContext} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -10,7 +10,6 @@ import * as Task from '@userActions/Task'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; -import {MenuItemGroupContext} from './MenuItemGroup'; type TaskHeaderActionButtonOnyxProps = { /** Current user session */ @@ -25,16 +24,6 @@ type TaskHeaderActionButtonProps = TaskHeaderActionButtonOnyxProps & { function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const {singleExecution} = useContext(MenuItemGroupContext) ?? {}; - - const onPressAction = () => { - const onPress = () => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)); - if (!singleExecution) { - onPress(); - return; - } - singleExecution(onPress)(); - }; if (!ReportUtils.canWriteInReport(report)) { return null; @@ -47,7 +36,7 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(onPressAction)} + onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} style={styles.flex1} /> diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index e0f5fed0192e..20d4f1fa175e 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -12,7 +12,6 @@ import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import * as Illustrations from '@components/Icon/Illustrations'; -import MenuItemGroup from '@components/MenuItemGroup'; import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestHeader from '@components/MoneyRequestHeader'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -659,92 +658,90 @@ function ReportScreen({ return ( - - + - - - {headerView} - {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( - - - - - + {headerView} + {ReportUtils.isTaskReport(report) && shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && ( + + + + - )} - - {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( - - )} - - - {shouldShowReportActionList && ( - - )} - - {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. - If we prevent rendering the report while they are loading then - we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} - {shouldShowSkeleton && } - - {isCurrentReportLoadedFromOnyx ? ( - setIsComposerFocus(true)} - onComposerBlur={() => setIsComposerFocus(false)} - report={report} - reportMetadata={reportMetadata} - reportNameValuePairs={reportNameValuePairs} - policy={policy} - pendingAction={reportPendingAction} - isComposerFullSize={!!isComposerFullSize} - isEmptyChat={isEmptyChat} - lastReportAction={lastReportAction} - /> - ) : null} - - - - - + )} + + {!!accountManagerReportID && ReportUtils.isConciergeChatReport(report) && isBannerVisible && ( + + )} + + + {shouldShowReportActionList && ( + + )} + + {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. + If we prevent rendering the report while they are loading then + we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} + {shouldShowSkeleton && } + + {isCurrentReportLoadedFromOnyx ? ( + setIsComposerFocus(true)} + onComposerBlur={() => setIsComposerFocus(false)} + report={report} + reportMetadata={reportMetadata} + reportNameValuePairs={reportNameValuePairs} + policy={policy} + pendingAction={reportPendingAction} + isComposerFullSize={!!isComposerFullSize} + isEmptyChat={isEmptyChat} + lastReportAction={lastReportAction} + /> + ) : null} + + + + + ); diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index f33a86ed2d46..011d5956be58 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,11 +1,10 @@ -import React, {useContext, useEffect} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; -import MenuItemGroup, {MenuItemGroupContext} from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -61,7 +60,6 @@ function ProfilePage({ const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); - const {waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const getPronouns = (): string => { const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; @@ -137,57 +135,55 @@ function ProfilePage({ icon={Illustrations.Profile} /> - - -
- {publicOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - brickRoadIndicator={detail.brickRoadIndicator} - /> - ))} -
-
- {isLoadingApp ? ( - - ) : ( - <> - {privateOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)) : () => Navigation.navigate(detail.pageRoute)} - /> - ))} - - )} -
-
-
+ +
+ {publicOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + brickRoadIndicator={detail.brickRoadIndicator} + /> + ))} +
+
+ {isLoadingApp ? ( + + ) : ( + <> + {privateOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + /> + ))} + + )} +
+
); From f0ba486330d804111b5c8a84e583ee9bdb1a1066 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Thu, 20 Jun 2024 10:48:08 +0200 Subject: [PATCH 03/44] Empty State Component for Search --- src/components/AccountingListSkeletonView.tsx | 4 +- src/components/EmptyStateComponent/index.tsx | 100 ++++++++++++++++++ src/components/EmptyStateComponent/types.ts | 29 +++++ src/components/Icon/index.tsx | 2 +- src/components/OptionsListSkeletonView.tsx | 4 +- src/components/Search.tsx | 21 +++- .../Skeletons/ItemListSkeletonView.tsx | 42 +++++--- ...ItemSkeleton.tsx => SearchRowSkeleton.tsx} | 14 ++- src/components/Skeletons/TableRowSkeleton.tsx | 52 +++++++++ src/styles/index.ts | 23 ++++ 10 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 src/components/EmptyStateComponent/index.tsx create mode 100644 src/components/EmptyStateComponent/types.ts rename src/components/Skeletons/{TableListItemSkeleton.tsx => SearchRowSkeleton.tsx} (77%) create mode 100644 src/components/Skeletons/TableRowSkeleton.tsx diff --git a/src/components/AccountingListSkeletonView.tsx b/src/components/AccountingListSkeletonView.tsx index b977903d3adc..d04e197edbab 100644 --- a/src/components/AccountingListSkeletonView.tsx +++ b/src/components/AccountingListSkeletonView.tsx @@ -4,12 +4,14 @@ import ItemListSkeletonView from './Skeletons/ItemListSkeletonView'; type AccountingListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacity?: boolean; }; -function AccountingListSkeletonView({shouldAnimate = true}: AccountingListSkeletonViewProps) { +function AccountingListSkeletonView({shouldAnimate = true, gradientOpacity = false}: AccountingListSkeletonViewProps) { return ( ( <> { + if (!event) { + return; + } + + if ('naturalSize' in event) { + setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + } else { + setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + } + }; + + let HeaderComponent; + switch (headerMediaType) { + case 'video': + HeaderComponent = ( + + ); + break; + case 'animation': + + 123 + ; + break; + default: + HeaderComponent = ( + + ); + } + + return ( + + + + + + + {HeaderComponent} + + {titleText} + {subtitleText} + + + + + + ); +} + +export default EmptyStateComponent; diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts new file mode 100644 index 000000000000..7a7cc9b4ff57 --- /dev/null +++ b/src/components/EmptyStateComponent/types.ts @@ -0,0 +1,29 @@ +import type DotLottieAnimation from '@components/LottieAnimations/types'; +import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; +import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type ValidSkeletons = typeof SearchRowSkeleton | typeof TableRowSkeleton; +type MediaTypes = 'video' | 'illustration' | 'animation'; + +type SharedProps = { + SkeletonComponent: ValidSkeletons; + titleText: string; + subtitleText: string; + buttonText?: string; + buttonAction?: () => void; + headerMediaType: T; +}; + +type MediaType = SharedProps & { + headerMedia: HeaderMedia; +}; + +type VideoProps = MediaType; +type IllustrationProps = MediaType; +type AnimationProps = MediaType; + +type EmptyStateComponentProps = VideoProps | IllustrationProps | AnimationProps; + +// eslint-disable-next-line import/prefer-default-export +export type {EmptyStateComponentProps}; diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index b4da5c0b0fa2..f49f74a6724a 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -1,4 +1,4 @@ -import type {ImageContentFit} from 'expo-image'; +import type {ImageContentFit, ImageStyle} from 'expo-image'; import React from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; diff --git a/src/components/OptionsListSkeletonView.tsx b/src/components/OptionsListSkeletonView.tsx index 1f09876b18d3..8b2a53b5c59e 100644 --- a/src/components/OptionsListSkeletonView.tsx +++ b/src/components/OptionsListSkeletonView.tsx @@ -17,12 +17,14 @@ function getLinedWidth(index: number): string { type OptionsListSkeletonViewProps = { shouldAnimate?: boolean; + gradientOpacity?: boolean; }; -function OptionsListSkeletonView({shouldAnimate = true}: OptionsListSkeletonViewProps) { +function OptionsListSkeletonView({shouldAnimate = true, gradientOpacity = false}: OptionsListSkeletonViewProps) { return ( { const lineWidth = getLinedWidth(itemIndex); diff --git a/src/components/Search.tsx b/src/components/Search.tsx index e4faa15bfb94..442bc5937b76 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -14,7 +14,6 @@ import * as SearchUtils from '@libs/SearchUtils'; import type {SearchColumnType, SortOrder} from '@libs/SearchUtils'; import Navigation from '@navigation/Navigation'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; -import EmptySearchView from '@pages/Search/EmptySearchView'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -23,10 +22,12 @@ import type {SearchQuery} from '@src/types/onyx/SearchResults'; import type SearchResults from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import EmptyStateComponent from './EmptyStateComponent'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; import type {ReportListItemType, TransactionListItemType} from './SelectionList/types'; -import TableListItemSkeleton from './Skeletons/TableListItemSkeleton'; +import SearchRowSkeleton from './Skeletons/SearchRowSkeleton'; +import TableRowSkeleton from './Skeletons/TableRowSkeleton'; type SearchProps = { query: SearchQuery; @@ -97,11 +98,21 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const shouldShowEmptyState = !isLoadingItems && isEmptyObject(searchResults?.data); if (isLoadingItems) { - return ; + return ; } if (shouldShowEmptyState) { - return ; + return ( + Navigation.navigate(ROUTES.CONCIERGE)} + buttonText="Go to Workspaces" + /> + ); } const openReport = (item: TransactionListItemType | ReportListItemType) => { @@ -188,7 +199,7 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { onEndReached={fetchMoreResults} listFooterContent={ isLoadingMoreItems ? ( - diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx index 5c46dbdddbfc..62e1e5afad13 100644 --- a/src/components/Skeletons/ItemListSkeletonView.tsx +++ b/src/components/Skeletons/ItemListSkeletonView.tsx @@ -1,5 +1,6 @@ import React, {useMemo, useState} from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -9,9 +10,19 @@ type ListItemSkeletonProps = { shouldAnimate?: boolean; renderSkeletonItem: (args: {itemIndex: number}) => React.ReactNode; fixedNumItems?: number; + gradientOpacity?: boolean; + itemViewStyle?: StyleProp; + itemViewHeight?: number; }; -function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNumItems}: ListItemSkeletonProps) { +function ItemListSkeletonView({ + shouldAnimate = true, + renderSkeletonItem, + fixedNumItems, + gradientOpacity = false, + itemViewStyle = {}, + itemViewHeight = CONST.LHN_SKELETON_VIEW_ITEM_HEIGHT, +}: ListItemSkeletonProps) { const theme = useTheme(); const themeStyles = useThemeStyles(); @@ -19,22 +30,25 @@ function ItemListSkeletonView({shouldAnimate = true, renderSkeletonItem, fixedNu const skeletonViewItems = useMemo(() => { const items = []; for (let i = 0; i < numItems; i++) { + const opacity = gradientOpacity ? 1 - i / numItems : 1; items.push( - - {renderSkeletonItem({itemIndex: i})} - , + + + + {renderSkeletonItem({itemIndex: i})} + + + , ); } return items; - }, [numItems, shouldAnimate, theme, themeStyles, renderSkeletonItem]); - + }, [numItems, shouldAnimate, theme, themeStyles, renderSkeletonItem, gradientOpacity, itemViewHeight, itemViewStyle]); return ( ( <> ( + <> + + + + + )} + /> + ); +} + +TableListItemSkeleton.displayName = 'TableListItemSkeleton'; + +export default TableListItemSkeleton; diff --git a/src/styles/index.ts b/src/styles/index.ts index b031e665594f..a12090ff275f 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -1663,6 +1663,7 @@ const styles = (theme: ThemeColors) => welcomeVideoNarrowLayout: { width: variables.onboardingModalWidth, + height: 500, }, onlyEmojisText: { @@ -5033,6 +5034,28 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, fontWeight: FontUtils.fontWeight.bold, }, + + skeletonBackground: { + flex: 1, + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + }, + + emptyStateForeground: (isSmallScreenWidth: boolean) => ({ + justifyContent: 'center', + alignItems: 'center', + height: '100%', + padding: isSmallScreenWidth ? 24 : 0, + }), + + emptyStateContent: (isSmallScreenWidth: boolean) => ({ + width: isSmallScreenWidth ? '100%' : 400, + backgroundColor: theme.cardBG, + borderRadius: variables.componentBorderRadiusLarge, + }), } satisfies Styles); type ThemeStyles = ReturnType; From 6d91057bf954ea8979b7da97de69a35382b3526c Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 20 Jun 2024 17:08:26 +0700 Subject: [PATCH 04/44] check active route approach --- src/components/ReportActionItem/TaskView.tsx | 6 ++++++ src/components/TaskHeaderActionButton.tsx | 13 ++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/TaskView.tsx b/src/components/ReportActionItem/TaskView.tsx index 43e896fe6578..ebe442dd8d75 100644 --- a/src/components/ReportActionItem/TaskView.tsx +++ b/src/components/ReportActionItem/TaskView.tsx @@ -96,6 +96,12 @@ function TaskView({report, ...props}: TaskViewProps) { { + if ( + Navigation.isActiveRoute(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID)) || + Navigation.isActiveRoute(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID)) + ) { + return; + } if (isCompleted) { Task.reopenTask(report); } else { diff --git a/src/components/TaskHeaderActionButton.tsx b/src/components/TaskHeaderActionButton.tsx index 0c7e603a4aa2..ccf3e22ecc5f 100644 --- a/src/components/TaskHeaderActionButton.tsx +++ b/src/components/TaskHeaderActionButton.tsx @@ -4,10 +4,12 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as Session from '@userActions/Session'; import * as Task from '@userActions/Task'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import Button from './Button'; @@ -36,7 +38,16 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps) isDisabled={!Task.canModifyTask(report, session?.accountID ?? -1)} medium text={translate(ReportUtils.isCompletedTaskReport(report) ? 'task.markAsIncomplete' : 'task.markAsComplete')} - onPress={Session.checkIfActionIsAllowed(() => (ReportUtils.isCompletedTaskReport(report) ? Task.reopenTask(report) : Task.completeTask(report)))} + onPress={Session.checkIfActionIsAllowed(() => { + if (Navigation.isActiveRoute(ROUTES.TASK_ASSIGNEE.getRoute(report.reportID)) || Navigation.isActiveRoute(ROUTES.REPORT_DESCRIPTION.getRoute(report.reportID))) { + return; + } + if (ReportUtils.isCompletedTaskReport(report)) { + Task.reopenTask(report); + } else { + Task.completeTask(report); + } + })} style={styles.flex1} /> From 0308f12e27abcd6e6486ebf1d37ebec5a79bcd16 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 20 Jun 2024 17:19:06 +0700 Subject: [PATCH 05/44] remove redundant changes --- src/components/MenuItem.tsx | 10 +- src/pages/home/ReportScreen.tsx | 4 +- src/pages/settings/Profile/ProfilePage.tsx | 101 +++++++++++---------- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index fad11c8e3a79..c1fe4270d4e1 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -394,7 +394,7 @@ function MenuItem( const StyleUtils = useStyleUtils(); const combinedStyle = [styles.popoverMenuItem, style]; const {shouldUseNarrowLayout} = useResponsiveLayout(); - const {isExecuting, singleExecution} = useContext(MenuItemGroupContext) ?? {}; + const {isExecuting, singleExecution, waitForNavigate} = useContext(MenuItemGroupContext) ?? {}; const isDeleted = style && Array.isArray(style) ? style.includes(styles.offlineFeedback.deleted) : false; const descriptionVerticalMargin = shouldShowDescriptionOnTop ? styles.mb1 : styles.mt1; @@ -469,11 +469,15 @@ function MenuItem( } if (onPress && event) { - if (!singleExecution) { + if (!singleExecution || !waitForNavigate) { onPress(event); return; } - singleExecution(onPress)(event); + singleExecution( + waitForNavigate(() => { + onPress(event); + }), + )(); } }; diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index 20d4f1fa175e..ade50c0e2c9b 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -719,8 +719,8 @@ function ReportScreen({ )} {/* Note: The ReportActionsSkeletonView should be allowed to mount even if the initial report actions are not loaded. - If we prevent rendering the report while they are loading then - we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} + If we prevent rendering the report while they are loading then + we'll unnecessarily unmount the ReportActionsView which will clear the new marker lines initial state. */} {shouldShowSkeleton && } {isCurrentReportLoadedFromOnyx ? ( diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index 011d5956be58..4c5ed88e6898 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; +import MenuItemGroup from '@components/MenuItemGroup'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -135,55 +136,57 @@ function ProfilePage({ icon={Illustrations.Profile} /> - -
- {publicOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - brickRoadIndicator={detail.brickRoadIndicator} - /> - ))} -
-
- {isLoadingApp ? ( - - ) : ( - <> - {privateOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - /> - ))} - - )} -
-
+ + +
+ {publicOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + brickRoadIndicator={detail.brickRoadIndicator} + /> + ))} +
+
+ {isLoadingApp ? ( + + ) : ( + <> + {privateOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + /> + ))} + + )} +
+
+
); From a99586158513d3f9bba965256477bb83039c57e1 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 21 Jun 2024 13:50:26 +0200 Subject: [PATCH 06/44] Remove backgroundColor --- src/components/EmptyStateComponent/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 496930d05208..1933df015b55 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -62,7 +62,7 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b default: HeaderComponent = ( Date: Mon, 24 Jun 2024 09:39:07 +0200 Subject: [PATCH 07/44] Animation case --- src/components/EmptyStateComponent/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 1933df015b55..18277565b780 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -56,7 +56,7 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b break; case 'animation': - 123 + Animation ; break; default: From 01333bcbc61a3c7c42e587c8ca149cc0d448e587 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 24 Jun 2024 15:16:50 +0200 Subject: [PATCH 08/44] Add animation and implement on Search page --- src/components/EmptyStateComponent/index.tsx | 41 +++++++------- src/components/EmptyStateComponent/types.ts | 2 + src/components/Icon/Expensicons.ts | 2 + src/components/ImageSVG/index.tsx | 2 +- src/components/Search.tsx | 15 +++--- .../Skeletons/ItemListSkeletonView.tsx | 54 +++++++++++++------ src/languages/en.ts | 4 ++ src/languages/es.ts | 4 ++ src/styles/index.ts | 5 ++ 9 files changed, 87 insertions(+), 42 deletions(-) diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index 18277565b780..a6bd07b94550 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -3,6 +3,7 @@ import React, {useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; import ImageSVG from '@components/ImageSVG'; +import Lottie from '@components/Lottie'; import Text from '@components/Text'; import VideoPlayer from '@components/VideoPlayer'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -19,7 +20,7 @@ type VideoLoadedEventType = { }; }; -function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, titleText, subtitleText}: EmptyStateComponentProps) { +function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, titleText, subtitleText, headerStyles}: EmptyStateComponentProps) { const styles = useThemeStyles(); const isSmallScreenWidth = getIsSmallScreenWidth(); @@ -55,19 +56,21 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b ); break; case 'animation': - - Animation - ; - break; - default: HeaderComponent = ( - ); + break; + case 'illustration': + HeaderComponent = ; + break; + default: + HeaderComponent = null; + break; } return ( @@ -80,16 +83,18 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b
- {HeaderComponent} + {HeaderComponent} {titleText} {subtitleText} - + {buttonText && buttonAction && ( + + )} diff --git a/src/components/EmptyStateComponent/types.ts b/src/components/EmptyStateComponent/types.ts index 7a7cc9b4ff57..a3ef4ad75f59 100644 --- a/src/components/EmptyStateComponent/types.ts +++ b/src/components/EmptyStateComponent/types.ts @@ -1,3 +1,4 @@ +import type {StyleProp, ViewStyle} from 'react-native'; import type DotLottieAnimation from '@components/LottieAnimations/types'; import type SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import type TableRowSkeleton from '@components/Skeletons/TableRowSkeleton'; @@ -12,6 +13,7 @@ type SharedProps = { subtitleText: string; buttonText?: string; buttonAction?: () => void; + headerStyles?: StyleProp; headerMediaType: T; }; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index c3e50cff3178..3ac4e346ccb5 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -118,6 +118,7 @@ import Meter from '@assets/images/meter.svg'; import MoneyBag from '@assets/images/money-bag.svg'; import MoneyCircle from '@assets/images/money-circle.svg'; import MoneySearch from '@assets/images/money-search.svg'; +import MoneyStack from '@assets/images/money-stack.svg'; import MoneyWaving from '@assets/images/money-waving.svg'; import Monitor from '@assets/images/monitor.svg'; import Mute from '@assets/images/mute.svg'; @@ -366,4 +367,5 @@ export { Clear, CheckCircle, CheckmarkCircle, + MoneyStack, }; diff --git a/src/components/ImageSVG/index.tsx b/src/components/ImageSVG/index.tsx index 3ce04a1a190a..cf58aa873584 100644 --- a/src/components/ImageSVG/index.tsx +++ b/src/components/ImageSVG/index.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type {SvgProps} from 'react-native-svg'; import type ImageSVGProps from './types'; -function ImageSVG({src, width = '100%', height = '100%', fill, hovered = false, pressed = false, style, pointerEvents, preserveAspectRatio}: ImageSVGProps) { +function ImageSVG({src, width, height = '100%', fill, hovered = false, pressed = false, style, pointerEvents, preserveAspectRatio}: ImageSVGProps) { const ImageSvgComponent = src as React.FC; const additionalProps: Pick = {}; diff --git a/src/components/Search.tsx b/src/components/Search.tsx index 442bc5937b76..449dfb4dfe9f 100644 --- a/src/components/Search.tsx +++ b/src/components/Search.tsx @@ -3,6 +3,7 @@ import type {StackNavigationProp} from '@react-navigation/stack'; import React, {useCallback, useEffect, useRef} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -23,11 +24,11 @@ import type SearchResults from '@src/types/onyx/SearchResults'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import EmptyStateComponent from './EmptyStateComponent'; +import LottieAnimations from './LottieAnimations'; import SelectionList from './SelectionList'; import SearchTableHeader from './SelectionList/SearchTableHeader'; import type {ReportListItemType, TransactionListItemType} from './SelectionList/types'; import SearchRowSkeleton from './Skeletons/SearchRowSkeleton'; -import TableRowSkeleton from './Skeletons/TableRowSkeleton'; type SearchProps = { query: SearchQuery; @@ -50,6 +51,7 @@ function isTransactionListItemType(item: TransactionListItemType | ReportListIte function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { const {isOffline} = useNetwork(); const styles = useThemeStyles(); + const {translate} = useLocalize(); const {isLargeScreenWidth} = useWindowDimensions(); const navigation = useNavigation>(); const lastSearchResultsRef = useRef>(); @@ -105,12 +107,11 @@ function Search({query, policyIDs, sortBy, sortOrder}: SearchProps) { return ( Navigation.navigate(ROUTES.CONCIERGE)} - buttonText="Go to Workspaces" + headerMediaType="animation" + headerMedia={LottieAnimations.Coin} + headerStyles={styles.activeComponentBG} + titleText={translate('search.searchResults.emptyState.title')} + subtitleText={translate('search.searchResults.emptyState.subtitle')} /> ); } diff --git a/src/components/Skeletons/ItemListSkeletonView.tsx b/src/components/Skeletons/ItemListSkeletonView.tsx index 62e1e5afad13..79b8dec1c183 100644 --- a/src/components/Skeletons/ItemListSkeletonView.tsx +++ b/src/components/Skeletons/ItemListSkeletonView.tsx @@ -1,6 +1,6 @@ -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; +import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -15,6 +15,14 @@ type ListItemSkeletonProps = { itemViewHeight?: number; }; +const getVerticalMargin = (style: StyleProp): number => { + if (!style) { + return 0; + } + const flattenStyle = style instanceof Array ? Object.assign({}, ...style) : style; + return Number((flattenStyle.marginVertical || 0) + (flattenStyle.marginTop || 0) + (flattenStyle.marginBottom || 0)); +}; + function ItemListSkeletonView({ shouldAnimate = true, renderSkeletonItem, @@ -27,13 +35,36 @@ function ItemListSkeletonView({ const themeStyles = useThemeStyles(); const [numItems, setNumItems] = useState(fixedNumItems ?? 0); + + const totalItemHeight = itemViewHeight + getVerticalMargin(itemViewStyle); + + const handleLayout = useCallback( + (event: LayoutChangeEvent) => { + if (fixedNumItems) { + return; + } + + const totalHeight = event.nativeEvent.layout.height; + + const newNumItems = Math.ceil(totalHeight / totalItemHeight); + + if (newNumItems !== numItems) { + setNumItems(newNumItems); + } + }, + [fixedNumItems, numItems, totalItemHeight], + ); + const skeletonViewItems = useMemo(() => { const items = []; for (let i = 0; i < numItems; i++) { const opacity = gradientOpacity ? 1 - i / numItems : 1; items.push( - - + + { - if (fixedNumItems) { - return; - } - - const newNumItems = Math.ceil(event.nativeEvent.layout.height / itemViewHeight); - if (newNumItems === numItems) { - return; - } - setNumItems(newNumItems); - }} + onLayout={handleLayout} > {skeletonViewItems} diff --git a/src/languages/en.ts b/src/languages/en.ts index 3a569801bb6a..3f0ca3299185 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2804,6 +2804,10 @@ export default { title: 'Nothing to show', subtitle: 'Try creating something using the green + button.', }, + emptyState: { + title: 'No expenses to display', + subtitle: 'Try creating something using the green + button.', + }, }, groupedExpenses: 'grouped expenses', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index a2118d55e43c..ad4c3973a6fd 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2843,6 +2843,10 @@ export default { title: 'No hay nada que ver aquí', subtitle: 'Por favor intenta crear algo usando el botón verde.', }, + emptyState: { + title: 'Sin gastos de exposición', + subtitle: 'Intenta crear algo utilizando el botón verde.', + }, }, groupedExpenses: 'gastos agrupados', }, diff --git a/src/styles/index.ts b/src/styles/index.ts index a12090ff275f..5bf96be3d3c1 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5056,6 +5056,11 @@ const styles = (theme: ThemeColors) => backgroundColor: theme.cardBG, borderRadius: variables.componentBorderRadiusLarge, }), + + emptyStateHeader: { + borderTopLeftRadius: variables.componentBorderRadiusLarge, + borderTopRightRadius: variables.componentBorderRadiusLarge, + }, } satisfies Styles); type ThemeStyles = ReturnType; From 9c096648ad8ab8e0572cba385c74114a90740f00 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 25 Jun 2024 08:25:13 +0200 Subject: [PATCH 09/44] Small fixes on search empty page, add new empty component on tags and category pages --- src/components/EmptyStateComponent/index.tsx | 6 +++--- src/components/EmptyStateComponent/types.ts | 4 ++-- src/components/Search.tsx | 16 ++-------------- .../Skeletons/ItemListSkeletonView.tsx | 2 +- src/components/Skeletons/SearchRowSkeleton.tsx | 1 + src/languages/en.ts | 4 ---- src/languages/es.ts | 4 ---- src/pages/Search/EmptySearchView.tsx | 14 ++++++++++---- .../categories/WorkspaceCategoriesPage.tsx | 11 ++++++++--- src/pages/workspace/tags/WorkspaceTagsPage.tsx | 11 ++++++++--- 10 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/components/EmptyStateComponent/index.tsx b/src/components/EmptyStateComponent/index.tsx index a6bd07b94550..24f75fa832f5 100644 --- a/src/components/EmptyStateComponent/index.tsx +++ b/src/components/EmptyStateComponent/index.tsx @@ -20,7 +20,7 @@ type VideoLoadedEventType = { }; }; -function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, titleText, subtitleText, headerStyles}: EmptyStateComponentProps) { +function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, buttonText, buttonAction, title, subtitle, headerStyles}: EmptyStateComponentProps) { const styles = useThemeStyles(); const isSmallScreenWidth = getIsSmallScreenWidth(); @@ -85,8 +85,8 @@ function EmptyStateComponent({SkeletonComponent, headerMediaType, headerMedia, b {HeaderComponent} - {titleText} - {subtitleText} + {title} + {subtitle} {buttonText && buttonAction && (