From b206430c3ecff1351d24825cfab1f7464d892e74 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 18 Jun 2024 20:10:43 +0200 Subject: [PATCH 1/7] report fields main page --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts | 1 + src/languages/en.ts | 6 + src/languages/es.ts | 6 + .../Navigators/FullScreenNavigator.tsx | 1 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/pages/workspace/WorkspaceInitialPage.tsx | 13 +- .../WorkspaceReportFieldsPage.tsx | 181 ++++++++++++++++++ 11 files changed, 219 insertions(+), 1 deletion(-) create mode 100644 src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c1fdd68951fa..73356a460eef 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -776,6 +776,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tax/:taxID/value', getRoute: (policyID: string, taxID: string) => `settings/workspaces/${policyID}/tax/${encodeURIComponent(taxID)}/value` as const, }, + WORKSPACE_REPORT_FIELDS: { + route: 'settings/workspaces/:policyID/reportFields', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/reportFields` as const, + }, WORKSPACE_DISTANCE_RATES: { route: 'settings/workspaces/:policyID/distance-rates', getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f884cca94ef5..79d317d6ecdc 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -283,6 +283,7 @@ const SCREENS = { TAGS_EDIT: 'Tags_Edit', TAG_EDIT: 'Tag_Edit', TAXES: 'Workspace_Taxes', + REPORT_FIELDS: 'Workspace_ReportFields', TAX_EDIT: 'Workspace_Tax_Edit', TAX_NAME: 'Workspace_Tax_Name', TAX_VALUE: 'Workspace_Tax_Value', diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index 5f10a7293457..dd1a65cdb75a 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -30,6 +30,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.WORKSPACE.MORE_FEATURES, SCREENS.WORKSPACE.TAGS, SCREENS.WORKSPACE.TAXES, + SCREENS.WORKSPACE.REPORT_FIELDS, SCREENS.WORKSPACE.DISTANCE_RATES, SCREENS.SEARCH.CENTRAL_PANE, ]; diff --git a/src/languages/en.ts b/src/languages/en.ts index bf3803c7606d..2ac8efb67ced 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2254,8 +2254,14 @@ export default { }, }, reportFields: { + addField: 'Add field', delete: 'Delete field', deleteConfirmation: 'Are you sure that you want to delete this field?', + emptyReportFields: { + title: "You haven't created any report fields", + subtitle: 'Add a custom field (text, date, or dropdown) that appears on reports.', + }, + subtitle: "Report fields apply to all spend and can be helpful when you'd like to prompt for extra information", }, tags: { tagName: 'Tag name', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4c900e23acc5..79720e7048e6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2290,8 +2290,14 @@ export default { }, }, reportFields: { + addField: 'Añadir campo', delete: 'Eliminar campos', deleteConfirmation: '¿Estás seguro de que quieres eliminar esta campos?', + emptyReportFields: { + title: "You haven't created any report fields", + subtitle: 'Add a custom field (text, date, or dropdown) that appears on reports.', + }, + subtitle: "Report fields apply to all spend and can be helpful when you'd like to prompt for extra information", }, tags: { tagName: 'Nombre de etiqueta', diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx index 64485872544f..f8c69fb6c218 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx @@ -29,6 +29,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.MORE_FEATURES]: () => require('../../../../pages/workspace/WorkspaceMoreFeaturesPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default as React.ComponentType, [SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default as React.ComponentType, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType, } satisfies Screens; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f91d290639ff..f2897587a043 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -83,6 +83,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT, SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS, ], + [SCREENS.WORKSPACE.REPORT_FIELDS]: [], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1b4288a9b3a9..db894a26fddd 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -785,6 +785,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.TAXES]: { path: ROUTES.WORKSPACE_TAXES.route, }, + [SCREENS.WORKSPACE.REPORT_FIELDS]: { + path: ROUTES.WORKSPACE_REPORT_FIELDS.route, + }, [SCREENS.WORKSPACE.DISTANCE_RATES]: { path: ROUTES.WORKSPACE_DISTANCE_RATES.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f90a91fe0f19..e71ca5ca1128 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -827,6 +827,9 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.TAXES]: { policyID: string; }; + [SCREENS.WORKSPACE.REPORT_FIELDS]: { + policyID: string; + }; [SCREENS.WORKSPACE.DISTANCE_RATES]: { policyID: string; }; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index a346461694a5..1933c14930f9 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -62,7 +62,8 @@ type WorkspaceMenuItem = { | typeof SCREENS.WORKSPACE.TAXES | typeof SCREENS.WORKSPACE.MORE_FEATURES | typeof SCREENS.WORKSPACE.PROFILE - | typeof SCREENS.WORKSPACE.MEMBERS; + | typeof SCREENS.WORKSPACE.MEMBERS + | typeof SCREENS.WORKSPACE.REPORT_FIELDS; }; type WorkspaceInitialPageOnyxProps = { @@ -107,6 +108,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc [CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]: policy?.areTagsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]: policy?.tax?.trackingEnabled, [CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: policy?.areConnectionsEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_REPORTFIELDS_ENABLED]: policy?.areReportFieldsEnabled, }), [policy], ) as PolicyFeatureStates; @@ -269,6 +271,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_REPORTFIELDS_ENABLED]) { + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.reportFields', + icon: Expensicons.Pencil, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.REPORT_FIELDS, + }); + } + protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.moreFeatures', icon: Expensicons.Gear, diff --git a/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx new file mode 100644 index 000000000000..928dc6148692 --- /dev/null +++ b/src/pages/workspace/reportFields/WorkspaceReportFieldsPage.tsx @@ -0,0 +1,181 @@ +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {ActivityIndicator, View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import TableListItem from '@components/SelectionList/TableListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import Text from '@components/Text'; +import WorkspaceEmptyStateSection from '@components/WorkspaceEmptyStateSection'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Tag from '@userActions/Policy/Tag'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PolicyReportField} from '@src/types/onyx/Policy'; + +type ReportFieldForList = ListItem & {value: string}; + +type WorkspaceTagsPageProps = StackScreenProps; + +function WorkspaceReportFieldsPage({ + route: { + params: {policyID}, + }, +}: WorkspaceTagsPageProps) { + const {isSmallScreenWidth} = useWindowDimensions(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + const isFocused = useIsFocused(); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const filteredPolicyFieldList = useMemo(() => { + if (!policy?.fieldList) { + return {}; + } + return Object.fromEntries(Object.entries(policy.fieldList).filter(([key]) => key !== 'text_title')); + }, [policy]); + const [selectedReportFields, setSelectedReportFields] = useState([]); + + const fetchTags = useCallback(() => { + Tag.openPolicyTagsPage(policyID); + }, [policyID]); + + const {isOffline} = useNetwork({onReconnect: fetchTags}); + + useFocusEffect(fetchTags); + + useEffect(() => { + if (isFocused) { + return; + } + setSelectedReportFields([]); + }, [isFocused]); + + const reportFieldsList = useMemo(() => { + if (!policy) { + return []; + } + return Object.values(filteredPolicyFieldList).map((reportField) => ({ + value: reportField.name, + keyForList: String(reportField.orderWeight), + orderWeight: reportField.orderWeight, + isSelected: selectedReportFields.find((selectedReportField) => selectedReportField.name === reportField.name) !== undefined, + text: policy.name, + rightElement: ( + + {reportField.type} + + + ), + })); + }, [filteredPolicyFieldList, policy, selectedReportFields, styles.flexRow]); + + const updateSelectedReportFields = (item: ReportFieldForList) => { + const updatedReportFields = selectedReportFields.find((selectedReportField) => selectedReportField.name === item.value) + ? selectedReportFields.filter((selectedReportField) => selectedReportField.name !== item.value) + : [...selectedReportFields, filteredPolicyFieldList[item.value]]; + setSelectedReportFields(updatedReportFields); + }; + + const isLoading = !isOffline && reportFieldsList === undefined; + + const getHeaderButtons = () => ( + +