diff --git a/src/CONST.ts b/src/CONST.ts index d0a7223897f84..1cee820f370bc 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1365,6 +1365,16 @@ const CONST = { BILL: 'bill', }, CHAT_TYPE: chatTypes, + HELP_TYPE: { + ...chatTypes, + CHAT_CONCIERGE: 'concierge', + EXPENSE_REPORT: 'expenseReport', + EXPENSE: 'expense', + CHAT: 'chat', + IOU: 'iou', + TASK: 'task', + INVOICE: 'invoice', + }, WORKSPACE_CHAT_ROOMS: { ANNOUNCE: '#announce', ADMINS: '#admins', @@ -2587,6 +2597,14 @@ const CONST = { SCAN: 'scan', PER_DIEM: 'per-diem', }, + EXPENSE_TYPE: { + DISTANCE: 'distance', + MANUAL: 'manual', + SCAN: 'scan', + PER_DIEM: 'per-diem', + EXPENSIFY_CARD: 'expensifyCard', + PENDING_EXPENSIFY_CARD: 'pendingExpensifyCard', + }, REPORT_ACTION_TYPE: { PAY: 'pay', CREATE: 'create', diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index ed7f0b54d0c76..3114f3dca356e 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -8,7 +8,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PinButton from '@components/PinButton'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; -import HelpButton from '@components/SidePane/HelpButton'; +import HelpButton from '@components/SidePane/HelpComponents/HelpButton'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/components/Navigation/TopBar.tsx b/src/components/Navigation/TopBar.tsx index e6ff26c43d7ab..98d0c266ab6ab 100644 --- a/src/components/Navigation/TopBar.tsx +++ b/src/components/Navigation/TopBar.tsx @@ -5,7 +5,7 @@ import Breadcrumbs from '@components/Breadcrumbs'; import LoadingBar from '@components/LoadingBar'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; -import HelpButton from '@components/SidePane/HelpButton'; +import HelpButton from '@components/SidePane/HelpComponents/HelpButton'; import Text from '@components/Text'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx index e922102ee0eba..b87e118014fa4 100644 --- a/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeaderInput.tsx @@ -18,7 +18,7 @@ import type {SearchQueryJSON, SearchQueryString} from '@components/Search/types' import {isSearchQueryItem} from '@components/SelectionList/Search/SearchQueryListItem'; import type {SearchQueryItem} from '@components/SelectionList/Search/SearchQueryListItem'; import type {SelectionListHandle} from '@components/SelectionList/types'; -import HelpButton from '@components/SidePane/HelpButton'; +import HelpButton from '@components/SidePane/HelpComponents/HelpButton'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; diff --git a/src/components/SidePane/Help/HelpContent.tsx b/src/components/SidePane/Help/HelpContent.tsx deleted file mode 100644 index 02c226f4e4433..0000000000000 --- a/src/components/SidePane/Help/HelpContent.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import {findFocusedRoute} from '@react-navigation/native'; -import React, {useEffect, useRef} from 'react'; -import HeaderGap from '@components/HeaderGap'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScrollView from '@components/ScrollView'; -import getHelpContent from '@components/SidePane/getHelpContent'; -import useEnvironment from '@hooks/useEnvironment'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; -import useRootNavigationState from '@hooks/useRootNavigationState'; -import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import {substituteRouteParameters} from '@libs/SidePaneUtils'; - -type HelpContentProps = { - closeSidePane: (shouldUpdateNarrow?: boolean) => void; -}; - -function HelpContent({closeSidePane}: HelpContentProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {isProduction} = useEnvironment(); - const {isExtraLargeScreenWidth} = useResponsiveLayout(); - const route = useRootNavigationState((state) => { - const params = (findFocusedRoute(state)?.params as Record) ?? {}; - const activeRoute = Navigation.getActiveRouteWithoutParams(); - return substituteRouteParameters(activeRoute, params); - }); - - const wasPreviousNarrowScreen = useRef(!isExtraLargeScreenWidth); - useEffect(() => { - // Close the side pane when the screen size changes from large to small - if (!isExtraLargeScreenWidth && !wasPreviousNarrowScreen.current) { - closeSidePane(true); - wasPreviousNarrowScreen.current = true; - } - - // Reset the trigger when the screen size changes back to large - if (isExtraLargeScreenWidth) { - wasPreviousNarrowScreen.current = false; - } - }, [isExtraLargeScreenWidth, closeSidePane]); - - return ( - <> - - closeSidePane(false)} - onCloseButtonPress={() => closeSidePane(false)} - shouldShowBackButton={!isExtraLargeScreenWidth} - shouldShowCloseButton={isExtraLargeScreenWidth} - shouldDisplayHelpButton={false} - /> - {getHelpContent(styles, route, isProduction)} - - ); -} - -HelpContent.displayName = 'HelpContent'; - -export default HelpContent; diff --git a/src/components/SidePane/HelpComponents/HelpBulletList.tsx b/src/components/SidePane/HelpComponents/HelpBulletList.tsx new file mode 100644 index 0000000000000..63bde1a278a8c --- /dev/null +++ b/src/components/SidePane/HelpComponents/HelpBulletList.tsx @@ -0,0 +1,26 @@ +import type {ReactNode} from 'react'; +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; +import CONST from '@src/CONST'; + +type HelpBulletListProps = { + styles: ThemeStyles; + items: ReactNode[]; +}; + +function HelpBulletList({items, styles}: HelpBulletListProps) { + return items.map((item, index) => ( + + {CONST.DOT_SEPARATOR} + {item} + + )); +} + +export default HelpBulletList; diff --git a/src/components/SidePane/HelpButton.tsx b/src/components/SidePane/HelpComponents/HelpButton.tsx similarity index 100% rename from src/components/SidePane/HelpButton.tsx rename to src/components/SidePane/HelpComponents/HelpButton.tsx diff --git a/src/components/SidePane/HelpComponents/HelpContent.tsx b/src/components/SidePane/HelpComponents/HelpContent.tsx new file mode 100644 index 0000000000000..02ad6db4e5600 --- /dev/null +++ b/src/components/SidePane/HelpComponents/HelpContent.tsx @@ -0,0 +1,89 @@ +import {findFocusedRoute} from '@react-navigation/native'; +import React, {useEffect, useMemo, useRef} from 'react'; +// Importing from the react-native-gesture-handler package instead of the `components/ScrollView` to fix scroll issue: +// https://github.com/react-native-modal/react-native-modal/issues/236 +import {ScrollView} from 'react-native-gesture-handler'; +import {useOnyx} from 'react-native-onyx'; +import HeaderGap from '@components/HeaderGap'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import getHelpContent from '@components/SidePane/HelpContent/getHelpContent'; +import useEnvironment from '@hooks/useEnvironment'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useRootNavigationState from '@hooks/useRootNavigationState'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {getHelpPaneReportType} from '@libs/ReportUtils'; +import {substituteRouteParameters} from '@libs/SidePaneUtils'; +import {getExpenseType} from '@libs/TransactionUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type HelpContentProps = { + closeSidePane: (shouldUpdateNarrow?: boolean) => void; +}; + +function HelpContent({closeSidePane}: HelpContentProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isProduction} = useEnvironment(); + const {isExtraLargeScreenWidth} = useResponsiveLayout(); + + const routeParams = useRootNavigationState((state) => (findFocusedRoute(state)?.params as Record) ?? {}); + const reportID = routeParams.reportID || CONST.DEFAULT_NUMBER_ID; + const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const [parentReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID || CONST.DEFAULT_NUMBER_ID}`, { + canEvict: false, + }); + const parentReportAction = report?.parentReportActionID ? parentReportActions?.[report.parentReportActionID] : undefined; + const linkedTransactionID = useMemo(() => (isMoneyRequestAction(parentReportAction) ? getOriginalMessage(parentReportAction)?.IOUTransactionID : undefined), [parentReportAction]); + const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${linkedTransactionID ?? CONST.DEFAULT_NUMBER_ID}`); + + const route = useMemo(() => { + const expenseType = getExpenseType(transaction); + const overrides = {reportID: expenseType ? `:${CONST.REPORT.HELP_TYPE.EXPENSE}/:${expenseType}` : `:${getHelpPaneReportType(report)}`}; + const activeRoute = Navigation.getActiveRouteWithoutParams(); + return substituteRouteParameters(activeRoute, routeParams, overrides); + }, [transaction, report, routeParams]); + + const wasPreviousNarrowScreen = useRef(!isExtraLargeScreenWidth); + useEffect(() => { + // Close the side pane when the screen size changes from large to small + if (!isExtraLargeScreenWidth && !wasPreviousNarrowScreen.current) { + closeSidePane(true); + wasPreviousNarrowScreen.current = true; + } + + // Reset the trigger when the screen size changes back to large + if (isExtraLargeScreenWidth) { + wasPreviousNarrowScreen.current = false; + } + }, [isExtraLargeScreenWidth, closeSidePane]); + + return ( + <> + + closeSidePane(false)} + onCloseButtonPress={() => closeSidePane(false)} + shouldShowBackButton={!isExtraLargeScreenWidth} + shouldShowCloseButton={isExtraLargeScreenWidth} + shouldDisplayHelpButton={false} + /> + + {getHelpContent(styles, route, isProduction)} + + + ); +} + +HelpContent.displayName = 'HelpContent'; + +export default HelpContent; diff --git a/src/components/SidePane/HelpComponents/HelpExpandable.tsx b/src/components/SidePane/HelpComponents/HelpExpandable.tsx new file mode 100644 index 0000000000000..06547e7eebd6e --- /dev/null +++ b/src/components/SidePane/HelpComponents/HelpExpandable.tsx @@ -0,0 +1,38 @@ +import React, {useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +type HelpExpandableProps = { + children: React.ReactNode; + styles: ThemeStyles; + containerStyle?: StyleProp; + title?: string; + moreText?: string; +}; + +function HelpExpandable({children, styles, containerStyle, title, moreText = '(more)'}: HelpExpandableProps) { + const [isExpanded, setIsExpanded] = useState(false); + + return ( + + + {title}{' '} + {!isExpanded && ( + setIsExpanded(true)} + > + {moreText} + + )} + + {isExpanded && children} + + ); +} + +HelpExpandable.displayName = 'ExpandableHelp'; + +export default HelpExpandable; diff --git a/src/components/SidePane/SidePaneOverlay.tsx b/src/components/SidePane/HelpComponents/HelpOverlay.tsx similarity index 88% rename from src/components/SidePane/SidePaneOverlay.tsx rename to src/components/SidePane/HelpComponents/HelpOverlay.tsx index a16cfb1b90846..339c6fef3d047 100644 --- a/src/components/SidePane/SidePaneOverlay.tsx +++ b/src/components/SidePane/HelpComponents/HelpOverlay.tsx @@ -5,7 +5,7 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -type SidePaneOverlayProps = { +type HelpOverlayProps = { /** Whether the side pane is displayed over RHP */ isRHPVisible: boolean; @@ -15,7 +15,7 @@ type SidePaneOverlayProps = { const easing = Easing.bezier(0.76, 0.0, 0.24, 1.0).factory(); -function SidePaneOverlay({isRHPVisible, onBackdropPress}: SidePaneOverlayProps) { +function HelpOverlay({isRHPVisible, onBackdropPress}: HelpOverlayProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -51,6 +51,6 @@ function SidePaneOverlay({isRHPVisible, onBackdropPress}: SidePaneOverlayProps) ); } -SidePaneOverlay.displayName = 'SidePaneOverlay'; +HelpOverlay.displayName = 'HelpOverlay'; -export default SidePaneOverlay; +export default HelpOverlay; diff --git a/src/components/SidePane/HelpContent/chat/admins.tsx b/src/components/SidePane/HelpContent/chat/admins.tsx new file mode 100644 index 0000000000000..4c64a80ad0ed1 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/admins.tsx @@ -0,0 +1,39 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function AdminsChatRoom({styles}: {styles: ThemeStyles}) { + return ( + <> + #admins + + + Talking with Concierge, your setup specialist, or your account manager - When you first create the workspace, Concierge and a + setup specialist will be added. Feel free to ask any setup questions you have about how to configure the workspace, onboard your team, connect your accounting, or + anything else you might need. + , + + Monitoring workspace changes - Every #admins room shows an audit trail of any configuration changes or significant events + happening inside the workspace. + , + + Chatting with other admins - The #admins room is a useful space for workspace admins to chat with each other about anything, + whether or not it relates to Expensify. + , + ]} + /> + + + ); +} + +export default AdminsChatRoom; diff --git a/src/components/SidePane/HelpContent/chat/concierge.tsx b/src/components/SidePane/HelpContent/chat/concierge.tsx new file mode 100644 index 0000000000000..ec0b86e19d7a7 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/concierge.tsx @@ -0,0 +1,18 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function Concierge({styles}: {styles: ThemeStyles}) { + return ( + <> + Concierge + + Concierge is available 24/7 to answer any question you have about anything, whether that's how to get set up, how to fix a problem, or general best practices. Concierge is a + bot, but is really smart, and can escalate you to a human whenever you want. Say hi, it's friendly! + + + ); +} + +export default Concierge; diff --git a/src/components/SidePane/HelpContent/chat/expense/expensifyCard.tsx b/src/components/SidePane/HelpContent/chat/expense/expensifyCard.tsx new file mode 100644 index 0000000000000..ab6656536fe51 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/expense/expensifyCard.tsx @@ -0,0 +1,16 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function ExpensifyCardExpense({styles}: {styles: ThemeStyles}) { + return ( + <> + Expensify Card + An "Expensify Card" expense corresponds to a "posted" (meaning, finalized by the bank) purchase. + Expensify Card expenses cannot be reimbursed as they are centrally paid by the bank account linked to the workspace. + + ); +} + +export default ExpensifyCardExpense; diff --git a/src/components/SidePane/HelpContent/chat/expense/index.tsx b/src/components/SidePane/HelpContent/chat/expense/index.tsx new file mode 100644 index 0000000000000..05b2621ab54f0 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/expense/index.tsx @@ -0,0 +1,43 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function ExpenseChat({styles}: {styles: ThemeStyles}) { + return ( + <> + Expense + + + Receipt - Attach a photo or document to this expense. + , + + Amount - The financial total of this transaction. + , + + Description - A general explanation of what this expense was for. + , + + Merchant - The business this purchase was made at. + , + + Date - The day on which the purchase was made. + , + ]} + /> + + + The expense chat is shared with everyone in the approval flow, and will maintain an audit trail of all historical changes. + + ); +} + +export default ExpenseChat; diff --git a/src/components/SidePane/HelpContent/chat/expense/manual.tsx b/src/components/SidePane/HelpContent/chat/expense/manual.tsx new file mode 100644 index 0000000000000..e8bbde72251a8 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/expense/manual.tsx @@ -0,0 +1,17 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function ManualExpense({styles}: {styles: ThemeStyles}) { + return ( + <> + Manual + + A "manual" expense has had all its details specified by the workspace member. It was not imported from any system, or scanned from a receipt. + + + ); +} + +export default ManualExpense; diff --git a/src/components/SidePane/HelpContent/chat/expense/pendingExpensifyCard.tsx b/src/components/SidePane/HelpContent/chat/expense/pendingExpensifyCard.tsx new file mode 100644 index 0000000000000..d416d1df2ddd3 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/expense/pendingExpensifyCard.tsx @@ -0,0 +1,20 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function ExpensifyCardPendingExpense({styles}: {styles: ThemeStyles}) { + return ( + <> + Expensify Card (pending) + + A "pending" Expensify Card expense represents a purchase that was recently made on the card, but has not yet "posted" – meaning, it has not been formally recognized as a + final, complete transaction. + + Any changes made to this expense will be preserved when the expense posts, typically 2-7 days later. + Pending transactions cannot be approved, as the final expense amount will not be confirmed until it posts. + + ); +} + +export default ExpensifyCardPendingExpense; diff --git a/src/components/SidePane/HelpContent/chat/expense/scan.tsx b/src/components/SidePane/HelpContent/chat/expense/scan.tsx new file mode 100644 index 0000000000000..14af094c38c2c --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/expense/scan.tsx @@ -0,0 +1,15 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function ScanExpense({styles}: {styles: ThemeStyles}) { + return ( + <> + Scanned + A "scanned" expense was created by extracting the relevant details using the Concierge AI. + + ); +} + +export default ScanExpense; diff --git a/src/components/SidePane/HelpContent/chat/expenseReport.tsx b/src/components/SidePane/HelpContent/chat/expenseReport.tsx new file mode 100644 index 0000000000000..39cfab55eebb1 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/expenseReport.tsx @@ -0,0 +1,30 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function ExpenseReportChat({styles}: {styles: ThemeStyles}) { + return ( + <> + Expense Report + + Is shared with everyone in the approval flow configured inside the workspace., + Will maintain an audit trail of all historical workflow actions (i.e., approvals)., + ]} + /> + + + Press the attach button to add more expenses, or press the header for more options. Press on any expense to go deeper. + + ); +} + +export default ExpenseReportChat; diff --git a/src/components/SidePane/HelpContent/chat/index.tsx b/src/components/SidePane/HelpContent/chat/index.tsx new file mode 100644 index 0000000000000..091ba317debcd --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/index.tsx @@ -0,0 +1,130 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import {View} from 'react-native'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import type {ThemeStyles} from '@styles/index'; + +function Inbox({styles}: {styles: ThemeStyles}) { + return ( + <> + Chat + + Chat is the foundation of New Expensify. Every expense, expense report, workspace, or member has an associated "chat", which you can use to record additional details, or + collaborate with others. Every chat has the following components: + + + Header + + This shows who you are chatting with (or what you are chatting about). You can press the header for more details on the chat, or additional actions to take upon it. + + + Comments + + + Text - Rich text messages stored securely and delivered via web, app, email, or SMS. + , + + Images & Documents - Insert photos, screenshots, movies, PDFs, or more, using copy/paste, drag/drop, or the attach button. + , + + Expenses - Share an expense in the chat, either to simply track and document it, or to submit for reimbursement. + , + + Tasks - Record a task, and optionally assign it to someone (or yourself!). + , + ]} + /> + + + Actions + + + React - Throw a ♥️😂🔥 like on anything! + , + + Reply in thread - Go deeper by creating a new chat on any comment. + , + + Mark unread - Flag it for reading later, at your convenience. + , + ]} + /> + + + Composer + + + Markdown - Format text using bold, italics, and{' '} + more. + , + + Mention - Invite or tag anyone in the world to any chat by putting an @ in front of their email address or phone number + (e.g., @awong@marslink.web, or @415-867-5309). + , + ]} + /> + + + + Inbox + The Inbox is a prioritized "to do" list, highlighting exactly what you need to do next. It consists of: + + Priorities + + Expense reports waiting on you, + Tasks assigned to you, + Chats that have mentioned you, + Anything you have pinned, + ]} + /> + + + Chats + + + Most Recent - Lists every chat, ordered by whichever was most recently active. + , + + Focus - Only lists chats with unread messages, sorted alphabetically. + , + ]} + /> + + + ); +} + +export default Inbox; diff --git a/src/components/SidePane/HelpContent/chat/workspace.tsx b/src/components/SidePane/HelpContent/chat/workspace.tsx new file mode 100644 index 0000000000000..d9c4d430fe354 --- /dev/null +++ b/src/components/SidePane/HelpContent/chat/workspace.tsx @@ -0,0 +1,36 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function WorkspaceChat({styles}: {styles: ThemeStyles}) { + return ( + <> + Workspace + + + + Create expense - This will submit an expense to the workspace for reimbursement. + , + + Split expense - This will split an expense between the member and the workspace (e.g., for a business meal that brings a + spouse). + , + ]} + /> + + + All past expense reports are processed here and stored for historical reference. + + ); +} + +export default WorkspaceChat; diff --git a/src/components/SidePane/HelpContent/getHelpContent.tsx b/src/components/SidePane/HelpContent/getHelpContent.tsx new file mode 100644 index 0000000000000..b3af376048f1b --- /dev/null +++ b/src/components/SidePane/HelpContent/getHelpContent.tsx @@ -0,0 +1,176 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type {ReactNode} from 'react'; +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; +import CONST from '@src/CONST'; +import Chat from './chat'; +import AdminsChatRoom from './chat/admins'; +import Concierge from './chat/concierge'; +import ExpenseChat from './chat/expense'; +import ExpensifyCardExpense from './chat/expense/expensifyCard'; +import ManualExpense from './chat/expense/manual'; +import ExpensifyCardPendingExpense from './chat/expense/pendingExpensifyCard'; +import ScanExpense from './chat/expense/scan'; +import ExpenseReportChat from './chat/expenseReport'; +import WorkspaceChat from './chat/workspace'; +import Search from './search'; +import Settings from './settings'; +import Workspaces from './settings/workspaces'; +import PolicyID from './settings/workspaces/policyID'; + +type ContentComponent = (props: {styles: ThemeStyles}) => ReactNode; + +type HelpContent = { + /** The content to display for this route */ + content: ContentComponent; + + /** Any children routes that this route has */ + children?: Record; + + /** Whether this route is an exact match or displays parent content */ + isExact?: boolean; +}; + +const helpContentMap: HelpContent = { + content: () => null, + children: { + r: { + content: Chat, + children: { + [`:${CONST.REPORT.HELP_TYPE.POLICY_ADMINS}`]: { + content: AdminsChatRoom, + }, + [`:${CONST.REPORT.HELP_TYPE.CHAT_CONCIERGE}`]: { + content: Concierge, + }, + [`:${CONST.REPORT.HELP_TYPE.POLICY_EXPENSE_CHAT}`]: { + content: WorkspaceChat, + }, + [`:${CONST.REPORT.HELP_TYPE.EXPENSE_REPORT}`]: { + content: ExpenseReportChat, + }, + [`:${CONST.REPORT.HELP_TYPE.EXPENSE}`]: { + content: ExpenseChat, + children: { + [`:${CONST.IOU.EXPENSE_TYPE.MANUAL}`]: { + content: ManualExpense, + }, + [`:${CONST.IOU.EXPENSE_TYPE.SCAN}`]: { + content: ScanExpense, + }, + [`:${CONST.IOU.EXPENSE_TYPE.EXPENSIFY_CARD}`]: { + content: ExpensifyCardExpense, + }, + [`:${CONST.IOU.EXPENSE_TYPE.PENDING_EXPENSIFY_CARD}`]: { + content: ExpensifyCardPendingExpense, + }, + }, + }, + }, + }, + home: { + content: Chat, + }, + search: { + content: Search, + }, + settings: { + content: Settings, + children: { + workspaces: { + content: Workspaces, + children: { + ':policyID': { + content: PolicyID, + }, + }, + }, + }, + }, + }, +}; + +type DiagnosticDataProps = { + /** The styles to apply to the diagnostic data */ + styles: ThemeStyles; + + /** The route that was attempted to be accessed */ + route: string; + + /** Whether the route is an exact match */ + isExactMatch?: boolean; + + /** Help content to display */ + children?: ReactNode; +}; + +function DiagnosticData({styles, route, children, isExactMatch}: DiagnosticDataProps) { + const diagnosticTitle = isExactMatch ? 'Help content found for route:' : 'Missing help content for route:'; + + return ( + <> + {!!children && ( + <> + {children} + + + )} + Diagnostic data (visible only on staging) + {diagnosticTitle} + {route} + + ); +} + +function getHelpContent(styles: ThemeStyles, route: string, isProduction: boolean): ReactNode { + const routeParts = route.substring(1).split('/'); + const helpContentComponents: ContentComponent[] = []; + let activeHelpContent = helpContentMap; + let isExactMatch = true; + + for (const part of routeParts) { + if (activeHelpContent?.children?.[part]) { + activeHelpContent = activeHelpContent.children[part]; + helpContentComponents.push(activeHelpContent.content); + } else { + if (helpContentComponents.length === 0) { + // eslint-disable-next-line react/no-unescaped-entities + helpContentComponents.push(() => We couldn't find any help content for this route.); + } + isExactMatch = false; + break; + } + } + + const content = helpContentComponents.reverse().map((HelpContentNode, index) => { + return ( + <> + + {index < helpContentComponents.length - 1 && } + + ); + }); + + if (isProduction) { + return content; + } + + return ( + + {content} + + ); +} + +export default getHelpContent; diff --git a/src/components/SidePane/HelpContent/search.tsx b/src/components/SidePane/HelpContent/search.tsx new file mode 100644 index 0000000000000..9fd460fba5628 --- /dev/null +++ b/src/components/SidePane/HelpContent/search.tsx @@ -0,0 +1,104 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function Search({styles}: {styles: ThemeStyles}) { + return ( + <> + Reports + Virtually all data can be analyzed and reported upon in the Reports page. The major elements of this page include: + + Data type + + + Expense - Individual standalone expenses. + , + + Expense reports - Groups of expenses processed in a batch. + , + + Chats - Comments written by you and others. + , + + Invoices - Expenses submitted to clients for payment. + , + + Trips - Travel expenses booked with Expensify Travel or scanned with SmartScan. + , + ]} + /> + + + Search + A quick method of narrowing the results by keyword or more. + + State filter + + All, + <> + Expenses/Expense/Invoices reports: + Draft - Only you can see that hasn't been shared yet., + Outstanding - Submitted to someone and awaiting action., + Approved - Approved, but awaiting payment., + Done - Fully processed, no further action needed., + Paid - Fully paid, no further action needed., + ]} + /> + , + <> + Chats: + Unread - Not seen yet by you., + Sent - Sent by you., + Attachments - Image, movie, or document., + Links - Hyperlinks., + Pinned - Highlighted by you as important., + ]} + /> + , + <> + Trips: + Current - Happening or in the future., Past - Already happened.]} + /> + , + ]} + /> + + + Results + + Select a row to see additional options., Tap on a row to see more detail.]} + /> + + + ); +} + +export default Search; diff --git a/src/components/SidePane/HelpContent/settings/index.tsx b/src/components/SidePane/HelpContent/settings/index.tsx new file mode 100644 index 0000000000000..c7f836b921199 --- /dev/null +++ b/src/components/SidePane/HelpContent/settings/index.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function Settings({styles}: {styles: ThemeStyles}) { + return ( + <> + Settings + + + Profile - Configure how you appear to others. + , + + Wallet - See and manage your credit cards and bank accounts. + , + + Preferences - Adjust how the app works for you. + , + + Security - Lock down how you and others access your account. + , + + Workspaces - Organize expenses for yourself and share with others. + , + + Subscriptions - Manage payment details and history. + , + + Domains - Advanced security and corporate card configuration. + , + + Switch to Expensify Classic - Battle tested and reliable. + , + + Save the World - Let Expensify.org help your favorite teacher! + , + ]} + /> + + + ); +} + +export default Settings; diff --git a/src/components/SidePane/HelpContent/settings/workspaces/index.tsx b/src/components/SidePane/HelpContent/settings/workspaces/index.tsx new file mode 100644 index 0000000000000..98755c7433d30 --- /dev/null +++ b/src/components/SidePane/HelpContent/settings/workspaces/index.tsx @@ -0,0 +1,68 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function Workspaces({styles}: {styles: ThemeStyles}) { + return ( + <> + Workspaces + + + + Categorize and submit expenses + , + + Approve and reimburse expenses + , + + Sync with accounting packages + , + + Connect to company card feeds + , + + Manage Expensify Cards + , + + Chat with colleagues, partners, and clients + , + … and lots more!, + ]} + /> + + + + + Collect workspaces start at $5/member, and include all the basics for running a small business. + , + + Control workspaces start at $9/member, and provide advanced capabilities, more powerful accounting sync, and more + sophisticated approval flows. + , + ]} + /> + + + In general you would create one Workspace for each company you manage. You can create and join as many workspaces as you like. + + ); +} + +export default Workspaces; diff --git a/src/components/SidePane/HelpContent/settings/workspaces/policyID.tsx b/src/components/SidePane/HelpContent/settings/workspaces/policyID.tsx new file mode 100644 index 0000000000000..26dbbf413eeb1 --- /dev/null +++ b/src/components/SidePane/HelpContent/settings/workspaces/policyID.tsx @@ -0,0 +1,83 @@ +/* eslint-disable react/no-unescaped-entities */ +import React from 'react'; +import BulletList from '@components/SidePane/HelpComponents/HelpBulletList'; +import ExpandableHelp from '@components/SidePane/HelpComponents/HelpExpandable'; +import Text from '@components/Text'; +import type {ThemeStyles} from '@styles/index'; + +function Workspace({styles}: {styles: ThemeStyles}) { + return ( + <> + Workspace + This is where you configure all the settings of the many features associated with your workspace. + + Default features + + + Overview - Configure how it appears to others. + , + + Members - Add/remove members and admins. + , + + Workflows - Configure submission, approval, and reimbursement. + , + + Categories - Group expenses into a chart of accounts. + , + + Expensify Card - Issue native Expensify Cards to employees. + , + + Accounting - Sync with external accounting systems. + , + ]} + /> + + + Optional features + + + Distance rates - Configure mileage reimbursement. + , + + Company card - Connect and manage third-party corporate card feeds. + , + + Per diem - Configure daily rates. + , + + Rules - Customize expense violations and set policy. + , + + Invoices - Collect revenue from customers. + , + + Tags - Group expenses by project or client. + , + + Taxes - Track VAT and other taxes. + , + + Report fields - Capture extra expense report information. + , + ]} + /> + + + ); +} + +export default Workspace; diff --git a/src/components/SidePane/Help/index.android.tsx b/src/components/SidePane/HelpModal/index.android.tsx similarity index 93% rename from src/components/SidePane/Help/index.android.tsx rename to src/components/SidePane/HelpModal/index.android.tsx index 5518bf1875eef..4d41bebac6313 100644 --- a/src/components/SidePane/Help/index.android.tsx +++ b/src/components/SidePane/HelpModal/index.android.tsx @@ -2,8 +2,8 @@ import {useFocusEffect} from '@react-navigation/native'; import React, {useCallback} from 'react'; import {BackHandler} from 'react-native'; import Modal from '@components/Modal'; +import HelpContent from '@components/SidePane/HelpComponents/HelpContent'; import CONST from '@src/CONST'; -import HelpContent from './HelpContent'; import type HelpProps from './types'; function Help({isPaneHidden, closeSidePane}: HelpProps) { diff --git a/src/components/SidePane/Help/index.ios.tsx b/src/components/SidePane/HelpModal/index.ios.tsx similarity index 89% rename from src/components/SidePane/Help/index.ios.tsx rename to src/components/SidePane/HelpModal/index.ios.tsx index 613040f1251c0..d249bdf316a9c 100644 --- a/src/components/SidePane/Help/index.ios.tsx +++ b/src/components/SidePane/HelpModal/index.ios.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Modal from '@components/Modal'; +import HelpContent from '@components/SidePane/HelpComponents/HelpContent'; import CONST from '@src/CONST'; -import HelpContent from './HelpContent'; import type HelpProps from './types'; function Help({isPaneHidden, closeSidePane}: HelpProps) { diff --git a/src/components/SidePane/Help/index.tsx b/src/components/SidePane/HelpModal/index.tsx similarity index 94% rename from src/components/SidePane/Help/index.tsx rename to src/components/SidePane/HelpModal/index.tsx index 659ad52ba4256..eb4d6cf0ca2bc 100644 --- a/src/components/SidePane/Help/index.tsx +++ b/src/components/SidePane/HelpModal/index.tsx @@ -6,14 +6,14 @@ import {useOnyx} from 'react-native-onyx'; // Modal from react-native can't be used here, as it would block interactions with the rest of the app import ModalPortal from 'react-native-web/dist/exports/Modal/ModalPortal'; import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal'; -import SidePaneOverlay from '@components/SidePane/SidePaneOverlay'; +import HelpContent from '@components/SidePane/HelpComponents/HelpContent'; +import HelpOverlay from '@components/SidePane/HelpComponents/HelpOverlay'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSafeAreaPaddings from '@hooks/useSafeAreaPaddings'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import HelpContent from './HelpContent'; import type HelpProps from './types'; function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: HelpProps) { @@ -60,7 +60,7 @@ function Help({sidePaneTranslateX, closeSidePane, shouldHideSidePaneBackdrop}: H {!shouldHideSidePaneBackdrop && ( - diff --git a/src/components/SidePane/Help/types.ts b/src/components/SidePane/HelpModal/types.ts similarity index 100% rename from src/components/SidePane/Help/types.ts rename to src/components/SidePane/HelpModal/types.ts diff --git a/src/components/SidePane/getHelpContent.tsx b/src/components/SidePane/getHelpContent.tsx deleted file mode 100644 index 3541ef9fc072d..0000000000000 --- a/src/components/SidePane/getHelpContent.tsx +++ /dev/null @@ -1,439 +0,0 @@ -/* eslint-disable react/no-unescaped-entities */ - -/* eslint-disable @typescript-eslint/naming-convention */ -import type {ReactNode} from 'react'; -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import type {ThemeStyles} from '@styles/index'; -import CONST from '@src/CONST'; - -type HelpContent = { - /** The content to display for this route */ - content?: (styles: ThemeStyles) => ReactNode; - - /** Any children routes that this route has */ - children?: Record; - - /** Whether this route is an exact match or displays parent content */ - isExact?: boolean; -}; - -const helpContentMap: Record = { - r: { - content: (styles: ThemeStyles) => ( - <> - Inbox - - Chat is the foundation of New Expensify. Every expense, expense report, workspace, or member has an associated "chat", which you can use to record additional details, or - collaborate with others. Every chat has the following components: - - - Header - - This shows who you are chatting with (or what you are chatting about). You can press the header for more details on the chat, or additional actions to take upon it. - - - Comments - The core of the chat are its comments, which come in many forms: - - {CONST.BULLET} Text - Rich text messages stored securely and delivered via web, app, email, or SMS. - - - {CONST.BULLET} Images & Documents - Insert photos, screenshots, movies, PDFs, or more, using copy/paste, drag/drop, or the attach - button - - - {CONST.BULLET} Expenses - Share an expense in the chat, either to simply track and document it, or to submit for reimbursement. - - - {CONST.BULLET} Tasks - Record a task, and optionally assign it to someone (or yourself!) - - - Actions - Hover (or long press) on a comment to see additional options, including: - - {CONST.BULLET} React - Throw a ♥️😂🔥 like on anything! - - - {CONST.BULLET} Reply in thread - Go deeper by creating a new chat on any comment. - - - {CONST.BULLET} Mark unread - Flag it for reading later, at your convenience. - - - Composer - Use the composer at the bottom to write new messages: - - {CONST.BULLET} Markdown - Format text using *bold*,{' '} - _italics_, and more. - - - {CONST.BULLET} Mention - Invite or tag anyone in the world to any chat by putting an @ in front of their email address or phone - number (eg, - @awong@marslink.web, or @415-867-5309). - - - {CONST.BULLET} Mention - Invite or tag anyone in the world to any chat by putting an @ in front of their email address or phone - number (eg, - @awong@marslink.web, or @415-867-5309). - - - ), - }, - home: { - content: (styles: ThemeStyles) => ( - <> - Inbox - - Chat is the foundation of New Expensify. Every expense, expense report, workspace, or member has an associated "chat", which you can use to record additional details, or - collaborate with others. Every chat has the following components: - - - Header - - This shows who you are chatting with (or what you are chatting about). You can press the header for more details on the chat, or additional actions to take upon it. - - - Comments - The core of the chat are its comments, which come in many forms: - - {CONST.BULLET} Text - Rich text messages stored securely and delivered via web, app, email, or SMS. - - - {CONST.BULLET} Images & Documents - Insert photos, screenshots, movies, PDFs, or more, using copy/paste, drag/drop, or the attach - button - - - {CONST.BULLET} Expenses - Share an expense in the chat, either to simply track and document it, or to submit for reimbursement. - - - {CONST.BULLET} Tasks - Record a task, and optionally assign it to someone (or yourself!) - - - Actions - Hover (or long press) on a comment to see additional options, including: - - {CONST.BULLET} React - Throw a ❤️😂🔥 or anything you like on anything! - - - {CONST.BULLET} Reply in thread - Go deeper by creating a new chat on any comment. - - - {CONST.BULLET} Mark unread - Flag it for reading later, at your convenience. - - - Composer - Use the composer at the bottom to write new messages: - - {CONST.BULLET} Markdown - Format text using *bold*,{' '} - _italics_, and more. - - - {CONST.BULLET} Mention - Invite or tag anyone in the world to any chat by putting an @ in front of their email address or phone - number (eg, - @awong@marslink.web, or @415-867-5309). - - - ), - }, - search: { - content: (styles: ThemeStyles) => ( - <> - Reports - Virtually all data can be analyzed and reported upon in the Reports page. The major elements of this page include: - - Data type - Start first by choosing the type of data you want to analyze, which can be: - - {CONST.BULLET} Expense - Individual standalone expenses. - - - {CONST.BULLET} Expense reports - Groups of expenses processed in a batch. - - - {CONST.BULLET} Chats - Comments written by you and others. - - - {CONST.BULLET} Invoices - Expenses submitted to clients for payment. - - - {CONST.BULLET} Trips - Travel expenses booked with Expensify Travel or scanned with SmartScan. - - - Search - A quick method of narrowing the results by keyword or more. - - State filter - Simple methods to filter the results by "state", including: - - {CONST.BULLET} All - Everything in every state. - - - Expenses/Expense Reports/Invoices - - {CONST.BULLET} Draft - Only you can see that hasn't been shared yet. - - - {CONST.BULLET} Outstanding - Submitted to someone and awaiting action. - - - {CONST.BULLET} Approved - Approved, but awaiting payment. - - - {CONST.BULLET} Done - Fully processed, no further action needed. - - - {CONST.BULLET} Paid - Fully paid, no further action needed. - - - Chats - - {CONST.BULLET} Unread - Not seen yet by you. - - - {CONST.BULLET} Sent - Sent by you. - - - {CONST.BULLET} Attachments - Image, movie, or document. - - - {CONST.BULLET} Links - Hyperlinks. - - - {CONST.BULLET} Pinned - Highlighted by you as important. - - - Trips - - {CONST.BULLET} Current - Happening or in the future. - - - {CONST.BULLET} Past - Already happened. - - - Results - The core of the Reports page are the search results themselves. - {CONST.BULLET} Select a row to see additional options. - {CONST.BULLET} Tap on a row to see more detail. - - ), - }, - settings: { - content: (styles: ThemeStyles) => ( - <> - Settings - Here is where you configure Expensify exactly to your specifications: - - - {CONST.BULLET} Profile - Configure how you appear to others. - - - {CONST.BULLET} Wallet - See and manage your credit cards and bank accounts. - - - {CONST.BULLET} Preferences - Adjust how the app works for you. - - - {CONST.BULLET} Security - Lock down how you and others access your account. - - - {CONST.BULLET} Workspaces - Organize expenses for yourself and share with others. - - - {CONST.BULLET} Subscriptions - Manage payment details and history. - - - {CONST.BULLET} Domains - Advanced security and corporate card configuration. - - - {CONST.BULLET} Switch to Expensify Classic - Battle-tested and reliable. - - - {CONST.BULLET} Save the World - Let Expensify.org help your favorite teacher! - - - ), - children: { - workspaces: { - content: (styles: ThemeStyles) => ( - <> - Settings > Workspaces - Workspaces allow for a wide range of features, including: - - - {CONST.BULLET} Categorize and submit expenses. - - - {CONST.BULLET} Approve and reimburse expenses. - - - {CONST.BULLET} Sync with accounting packages. - - - {CONST.BULLET} Connect to company card feeds. - - - {CONST.BULLET} Manage Expensify Cards. - - - {CONST.BULLET} Chat with colleagues, partners, and clients. - - {CONST.BULLET} … and lots more! - - Workspace Variations - Workspaces come in two variations: - - - {CONST.BULLET} Collect workspaces start at $5/member, and include all the basics for running a small business. - - - {CONST.BULLET} Control workspaces start at $9/member, and provide advanced capabilities, more powerful accounting sync, and - more sophisticated approval flows. - - - Managing Workspaces - In general, you would create one Workspace for each company you manage. You can create and join as many workspaces as you like. - - ), - children: { - ':policyID': { - content: (styles: ThemeStyles) => ( - <> - Workspaces - This is where you configure all the settings of the many features associated with your workspace. - - Default Features - Here are the features that are enabled by default: - - - {CONST.BULLET} Overview - Configure how it appears to others. - - - {CONST.BULLET} Members - Add/remove members and admins. - - - {CONST.BULLET} Workflows - Configure submission, approval, and reimbursement. - - - {CONST.BULLET} Categories - Group expenses into a chart of accounts. - - - {CONST.BULLET} Expensify Card - Issue native Expensify Cards to employees. - - - {CONST.BULLET} Accounting - Sync with external accounting systems. - - - Optional Features - - These can be enabled via More Features: - - - - {CONST.BULLET} Distance rates - Configure mileage reimbursement. - - - {CONST.BULLET} Company card - Connect and manage third-party corporate card feeds. - - - {CONST.BULLET} Per diem - Configure daily rates. - - - {CONST.BULLET} Rules - Customize expense violations and set policy. - - - {CONST.BULLET} Invoices - Collect revenue from customers. - - - {CONST.BULLET} Tags - Group expenses by project or client. - - - {CONST.BULLET} Taxes - Track VAT and other taxes. - - - Report Fields - Capture extra expense report information. - - ), - }, - }, - }, - }, - }, -}; - -type DiagnosticDataProps = { - styles: ThemeStyles; - route: string; - currentRoute?: string; - isExactMatch?: boolean; - children?: ReactNode; -}; - -function DiagnosticData({styles, route, currentRoute, children, isExactMatch}: DiagnosticDataProps) { - const diagnosticTitle = isExactMatch ? 'Help content found for route:' : 'Missing help content for route:'; - - return ( - <> - {!!children && ( - <> - {children} - - - )} - {diagnosticTitle} - {route} - {!isExactMatch && !!currentRoute && ( - <> - Using content from: - {currentRoute} - - )} - - ); -} - -function getHelpContent(styles: ThemeStyles, route: string, isProduction: boolean): ReactNode { - const [firstPart, ...routeParts] = route.substring(1).split('/'); - const currentRoute = [firstPart]; - let currentNode: HelpContent = helpContentMap[firstPart]; - let isExactMatch = true; - - for (const part of routeParts) { - if (currentNode?.children?.[part]) { - currentNode = currentNode.children[part]; - currentRoute.push(part); - isExactMatch = true; - } else { - isExactMatch = false; - break; - } - } - - if (currentNode?.content) { - if (isProduction) { - return currentNode.content(styles); - } - - return ( - - {currentNode.content(styles)} - - ); - } - - return ( - - ); -} - -export default getHelpContent; diff --git a/src/components/SidePane/index.tsx b/src/components/SidePane/index.tsx index 715135a33468e..73aeab56434fa 100644 --- a/src/components/SidePane/index.tsx +++ b/src/components/SidePane/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import useSidePane from '@hooks/useSidePane'; -import Help from './Help'; +import Help from './HelpModal'; function SidePane() { const {shouldHideSidePane, isPaneHidden, sidePaneTranslateX, shouldHideSidePaneBackdrop, closeSidePane} = useSidePane(); diff --git a/src/hooks/useSidePane.ts b/src/hooks/useSidePane.ts index 74150028c04cb..d645f9bd55faa 100644 --- a/src/hooks/useSidePane.ts +++ b/src/hooks/useSidePane.ts @@ -31,9 +31,14 @@ function useSidePane() { const [sidePaneNVP] = useOnyx(ONYXKEYS.NVP_SIDE_PANE); const [language] = useOnyx(ONYXKEYS.NVP_PREFERRED_LOCALE); - const [isAttachmentModalVisible = false] = useOnyx(ONYXKEYS.MODAL, {selector: (modal) => modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED}); + const [isModalCenteredVisible = false] = useOnyx(ONYXKEYS.MODAL, { + selector: (modal) => + modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_SWIPABLE_TO_RIGHT || + modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE || + modal?.type === CONST.MODAL.MODAL_TYPE.CENTERED_SMALL, + }); const isLanguageUnsupported = language !== CONST.LOCALES.EN; - const isPaneHidden = isSidePaneHidden(sidePaneNVP, isExtraLargeScreenWidth) || isLanguageUnsupported || isAttachmentModalVisible; + const isPaneHidden = isSidePaneHidden(sidePaneNVP, isExtraLargeScreenWidth) || isLanguageUnsupported || isModalCenteredVisible; const sidePaneWidth = shouldUseNarrowLayout ? windowWidth : variables.sideBarWidth; const shouldApplySidePaneOffset = isExtraLargeScreenWidth && !isPaneHidden; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 571b5ec68f4a7..2e6ee54ba0c17 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2047,6 +2047,38 @@ function isMoneyRequestReport(reportOrID: OnyxInputOrEntry | SearchRepor return isIOUReport(report) || isExpenseReport(report); } +/** + * Determines the help pane report type based on the given report. + */ +function getHelpPaneReportType(report: OnyxEntry): ValueOf | undefined { + if (!report) { + return undefined; + } + + if (isConciergeChatReport(report)) { + return CONST.REPORT.HELP_TYPE.CHAT_CONCIERGE; + } + + if (report?.chatType) { + return getChatType(report); + } + + switch (report?.type) { + case CONST.REPORT.TYPE.EXPENSE: + return CONST.REPORT.HELP_TYPE.EXPENSE_REPORT; + case CONST.REPORT.TYPE.CHAT: + return CONST.REPORT.HELP_TYPE.CHAT; + case CONST.REPORT.TYPE.IOU: + return CONST.REPORT.HELP_TYPE.IOU; + case CONST.REPORT.TYPE.INVOICE: + return CONST.REPORT.HELP_TYPE.INVOICE; + case CONST.REPORT.TYPE.TASK: + return CONST.REPORT.HELP_TYPE.TASK; + default: + return undefined; + } +} + /** * Checks if a report contains only Non-Reimbursable transactions */ @@ -9717,6 +9749,7 @@ export { getIntegrationIcon, canBeExported, isExported, + getHelpPaneReportType, hasOnlyNonReimbursableTransactions, getReportLastMessage, getReportLastVisibleActionCreated, diff --git a/src/libs/SidePaneUtils.ts b/src/libs/SidePaneUtils.ts index 1b9cd04cdc4c6..82965763e8705 100644 --- a/src/libs/SidePaneUtils.ts +++ b/src/libs/SidePaneUtils.ts @@ -2,11 +2,12 @@ * This function is used to substitute the route parameters in the route string with the actual parameter names * * Example: - * route: /workspaces/123/rules/456 - * params: {workspaceID: '123', ruleID: '456'} - * result: /workspaces/:workspaceID/rules/:ruleID + * route: /workspaces/123/rules/456/777 + * params: {workspaceID: '123', ruleID: '456', reportID: '777'} + * overrides: {reportID: 'expense'} + * result: /workspaces/:workspaceID/rules/:ruleID/expense */ -function substituteRouteParameters(route: string, params: Record): string { +function substituteRouteParameters(route: string, params: Record, overrides?: Record): string { let updatedRoute = route; function searchAndReplace(obj: Record) { @@ -20,8 +21,9 @@ function substituteRouteParameters(route: string, params: Record); } else if (typeof value === 'string') { + const keyOverride = overrides?.[key]; const regex = new RegExp(`\\b${value}\\b`, 'g'); - updatedRoute = updatedRoute.replace(regex, `:${key}`); + updatedRoute = updatedRoute.replace(regex, keyOverride ?? `:${key}`); } } } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index deb9d96e66f36..1861d1b2b36d0 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -177,6 +177,25 @@ function getRequestType(transaction: OnyxEntry): IOURequestType { return CONST.IOU.REQUEST_TYPE.MANUAL; } +/** + * Determines the expense type of a given transaction. + */ +function getExpenseType(transaction: OnyxEntry): ValueOf | undefined { + if (!transaction) { + return undefined; + } + + if (isExpensifyCardTransaction(transaction)) { + if (isPending(transaction)) { + return CONST.IOU.EXPENSE_TYPE.PENDING_EXPENSIFY_CARD; + } + + return CONST.IOU.EXPENSE_TYPE.EXPENSIFY_CARD; + } + + return getRequestType(transaction); +} + function isManualRequest(transaction: Transaction): boolean { // This is used during the expense creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { @@ -1481,6 +1500,7 @@ export { getUpdatedTransaction, getDescription, getRequestType, + getExpenseType, isManualRequest, isScanRequest, getAmount, diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index ec77fa771bf86..53e67ea950ade 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -16,7 +16,7 @@ import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; -import HelpButton from '@components/SidePane/HelpButton'; +import HelpButton from '@components/SidePane/HelpComponents/HelpButton'; import SubscriptAvatar from '@components/SubscriptAvatar'; import TaskHeaderActionButton from '@components/TaskHeaderActionButton'; import Text from '@components/Text'; diff --git a/tests/unit/SidePaneUtilsTest.ts b/tests/unit/SidePaneUtilsTest.ts index c94f42c6585e7..99f975df1e193 100644 --- a/tests/unit/SidePaneUtilsTest.ts +++ b/tests/unit/SidePaneUtilsTest.ts @@ -72,4 +72,39 @@ describe('substituteRouteParameters', () => { const params = {reportID: '321'}; expect(substituteRouteParameters(route, params)).toBe('/analysis/:reportID/report321/details'); }); + + test('should apply simple route parameter overrides', () => { + const route = '/workspaces/123/rules/456'; + const params = {workspaceID: '123', ruleID: '456'}; + const overrides = {workspaceID: 'workspace', ruleID: 'rule'}; + expect(substituteRouteParameters(route, params, overrides)).toBe('/workspaces/workspace/rules/rule'); + }); + + test('should apply overrides to repeated parameters in the route', () => { + const route = '/reports/123/report/123/details'; + const params = {id: '123'}; + const overrides = {id: 'expense'}; + expect(substituteRouteParameters(route, params, overrides)).toBe('/reports/expense/report/expense/details'); + }); + + test('should correctly replace a parameter with a multi-segment override', () => { + const route = '/transactions/555/audit/555/details'; + const params = {transactionID: '555'}; + const overrides = {transactionID: ':record/:subtype'}; + expect(substituteRouteParameters(route, params, overrides)).toBe('/transactions/:record/:subtype/audit/:record/:subtype/details'); + }); + + test('should prioritize overrides over default substitution', () => { + const route = '/projects/999/tasks/888'; + const params = {projectID: '999', taskID: '888'}; + const overrides = {taskID: 'task-replace'}; + expect(substituteRouteParameters(route, params, overrides)).toBe('/projects/:projectID/tasks/task-replace'); + }); + + test('should apply overrides even when override matches another route param', () => { + const route = '/workspaces/123/rules/456'; + const params = {workspaceID: '123', ruleID: '456'}; + const overrides = {workspaceID: '456', ruleID: 'rule'}; + expect(substituteRouteParameters(route, params, overrides)).toBe('/workspaces/rule/rules/rule'); + }); });