diff --git a/src/components/Accordion/index.tsx b/src/components/Accordion/index.tsx index 9715f3902c03..d70b8db835dd 100644 --- a/src/components/Accordion/index.tsx +++ b/src/components/Accordion/index.tsx @@ -24,6 +24,7 @@ type AccordionProps = { function Accordion({isExpanded, children, duration = 300, isToggleTriggered, style}: AccordionProps) { const height = useSharedValue(0); + const isAnimating = useSharedValue(false); const derivedHeight = useDerivedValue(() => { if (!isToggleTriggered.get()) { @@ -41,10 +42,20 @@ function Accordion({isExpanded, children, duration = 300, isToggleTriggered, sty return isExpanded.get() ? 1 : 0; } - return withTiming(isExpanded.get() ? 1 : 0, { - duration, - easing: Easing.inOut(Easing.quad), - }); + isAnimating.set(true); + return withTiming( + isExpanded.get() ? 1 : 0, + { + duration, + easing: Easing.inOut(Easing.quad), + }, + (finished) => { + if (!finished || !isExpanded.get()) { + return; + } + isAnimating.set(false); + }, + ); }); const animatedStyle = useAnimatedStyle(() => { @@ -52,12 +63,16 @@ function Accordion({isExpanded, children, duration = 300, isToggleTriggered, sty return { height: 0, opacity: 0, + display: 'none', }; } return { height: !isToggleTriggered.get() ? undefined : derivedHeight.get(), + maxHeight: !isToggleTriggered.get() ? undefined : derivedHeight.get(), opacity: derivedOpacity.get(), + overflow: isAnimating.get() ? 'hidden' : 'visible', + display: isExpanded.get() ? 'inline' : 'none', }; }); diff --git a/src/hooks/useAccordionAnimation.ts b/src/hooks/useAccordionAnimation.ts new file mode 100644 index 000000000000..d3995493ff8e --- /dev/null +++ b/src/hooks/useAccordionAnimation.ts @@ -0,0 +1,26 @@ +import {useEffect} from 'react'; +import {useSharedValue} from 'react-native-reanimated'; + +/** + * @returns two values: isExpanded, which manages the expansion of the accordion component, + * and shouldAnimateAccordionSection, which determines whether we should animate + * the expanding and collapsing of the accordion based on changes in isExpanded. + */ +function useAccordionAnimation(isExpanded: boolean) { + const isAccordionExpanded = useSharedValue(isExpanded); + const shouldAnimateAccordionSection = useSharedValue(false); + const hasMounted = useSharedValue(false); + + useEffect(() => { + isAccordionExpanded.set(isExpanded); + if (hasMounted.get()) { + shouldAnimateAccordionSection.set(true); + } else { + hasMounted.set(true); + } + }, [hasMounted, isAccordionExpanded, isExpanded, shouldAnimateAccordionSection]); + + return {isAccordionExpanded, shouldAnimateAccordionSection}; +} + +export default useAccordionAnimation; diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx index 2d2480835f6e..fa410a0e376b 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -1,10 +1,12 @@ import React, {useMemo} from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import {areSettingsInErrorFields, getCurrentSageIntacctEntityName, settingsPendingAction} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -17,7 +19,7 @@ import { updateSageIntacctSyncReimbursedReports, updateSageIntacctSyncReimbursementAccountID, } from '@userActions/connections/SageIntacct'; -import * as Policy from '@userActions/Policy/Policy'; +import {clearSageIntacctErrorField} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {SageIntacctDataElement} from '@src/types/onyx/Policy'; @@ -34,6 +36,8 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { const {importEmployees, autoSync, sync, pendingFields, errorFields} = policy?.connections?.intacct?.config ?? {}; const {data, config} = policy?.connections?.intacct ?? {}; + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!sync?.syncReimbursedReports); + const toggleSections = useMemo( () => [ { @@ -42,8 +46,8 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { isActive: !!autoSync?.enabled, onToggle: (enabled: boolean) => updateSageIntacctAutoSync(policyID, enabled), subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED], - error: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED), - onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED), + error: getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED), + onCloseError: () => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.AUTO_SYNC_ENABLED), }, { label: translate('workspace.sageIntacct.inviteEmployees'), @@ -54,12 +58,10 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { updateSageIntacctApprovalMode(policyID, enabled); }, subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE], - error: - ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES) ?? - ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE), + error: getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES) ?? getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE), onCloseError: () => { - Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES); - Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE); + clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.IMPORT_EMPLOYEES); + clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.APPROVAL_MODE); }, }, { @@ -75,9 +77,9 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { } }, subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS], - error: ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS), + error: getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS), onCloseError: () => { - Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS); + clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.SYNC_REIMBURSED_REPORTS); }, }, ], @@ -113,20 +115,23 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { /> ))} - {!!sync?.syncReimbursedReports && ( + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PAYMENT_ACCOUNT.getRoute(policyID))} brickRoadIndicator={areSettingsInErrorFields([CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID], errorFields) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> - )} + ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx index 91256a3c0f4e..e45fedc51fc1 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx @@ -1,26 +1,24 @@ import React from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, getSageIntacctNonReimbursableActiveDefaultVendor, settingsPendingAction} from '@libs/PolicyUtils'; -import type {MenuItem, ToggleItem} from '@pages/workspace/accounting/intacct/types'; +import type {ExtendedMenuItemWithSubscribedSettings, MenuItemToRender} from '@pages/workspace/accounting/intacct/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; -import * as Policy from '@userActions/Policy/Policy'; +import {clearSageIntacctErrorField} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {getDefaultVendorName} from './utils'; -type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; - -type ToggleItemWithKey = ToggleItem & {key: string}; - function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; @@ -29,8 +27,31 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP const activeDefaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy); const defaultVendorName = getDefaultVendorName(activeDefaultVendor, intacctData?.vendors); + const expandedCondition = !( + !config?.export.nonReimbursable || + (config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && !config?.export.nonReimbursableCreditCardChargeDefaultVendor) + ); + + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(expandedCondition); + + const renderDefault = (item: MenuItemToRender) => { + return ( + + + + ); + }; - const menuItems: Array = [ + const menuItems: ExtendedMenuItemWithSubscribedSettings[] = [ { type: 'menuitem', title: config?.export.nonReimbursable @@ -62,27 +83,38 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP onToggle: (enabled) => { const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor, config?.export.nonReimbursableCreditCardChargeDefaultVendor); + isAccordionExpanded.set(enabled); + shouldAnimateAccordionSection.set(true); }, - onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR), + onCloseError: () => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR), pendingAction: settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR], config?.pendingFields), - errors: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR), + errors: getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR), shouldHide: config?.export.nonReimbursable !== CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE, }, { - type: 'menuitem', - title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), - description: translate('workspace.sageIntacct.defaultVendor'), - onPress: () => { - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase())); - }, - subscribedSettings: [ - config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL - ? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR - : CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, + type: 'accordion', + children: [ + { + type: 'menuitem', + title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.sageIntacct.defaultVendor'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE.toLowerCase())); + }, + subscribedSettings: [ + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR + : CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, + ], + shouldHide: + !config?.export.nonReimbursable || + (config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && + !config?.export.nonReimbursableCreditCardChargeDefaultVendor), + }, ], - shouldHide: - !config?.export.nonReimbursable || - (config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && !config?.export.nonReimbursableCreditCardChargeDefaultVendor), + shouldHide: false, + shouldExpand: isAccordionExpanded, + shouldAnimateSection: shouldAnimateAccordionSection, }, ]; @@ -114,21 +146,17 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyConnectionsP wrapperStyle={[styles.mv3, styles.ph5]} /> ); - default: + case 'accordion': return ( - - - + {item.children.map((child) => renderDefault(child))} + ); + default: + return renderDefault(item); } })} diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx index 10edee426ff5..12265912fc9e 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -1,26 +1,24 @@ import React from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; -import type {MenuItem, ToggleItem} from '@pages/workspace/accounting/intacct/types'; +import type {ExtendedMenuItemWithSubscribedSettings, MenuItemToRender} from '@pages/workspace/accounting/intacct/types'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; -import * as Policy from '@userActions/Policy/Policy'; +import {clearSageIntacctErrorField} from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import {getDefaultVendorName} from './utils'; -type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; - -type ToggleItemWithKey = ToggleItem & {key: string}; - function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; @@ -30,7 +28,26 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProp const defaultVendorName = getDefaultVendorName(reimbursableExpenseReportDefaultVendor, intacctData?.vendors); - const menuItems: Array = [ + const expandedCondition = !(reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT || !reimbursableExpenseReportDefaultVendor); + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(expandedCondition); + + const renderDefault = (item: MenuItemToRender) => { + return ( + + + + ); + }; + const menuItems: ExtendedMenuItemWithSubscribedSettings[] = [ { type: 'menuitem', title: reimbursable ? translate(`workspace.sageIntacct.reimbursableExpenses.values.${reimbursable}`) : translate('workspace.sageIntacct.notConfigured'), @@ -50,21 +67,31 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProp onToggle: (enabled) => { const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor, config?.export.reimbursableExpenseReportDefaultVendor); + isAccordionExpanded.set(enabled); + shouldAnimateAccordionSection.set(true); }, - onCloseError: () => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR), + onCloseError: () => clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR), pendingAction: settingsPendingAction([CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR], config?.pendingFields), - errors: ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR), + errors: getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR), shouldHide: reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT, }, { - type: 'menuitem', - title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), - description: translate('workspace.sageIntacct.defaultVendor'), - onPress: () => { - Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)); - }, - subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR], - shouldHide: reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT || !reimbursableExpenseReportDefaultVendor, + type: 'accordion', + children: [ + { + type: 'menuitem', + title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), + description: translate('workspace.sageIntacct.defaultVendor'), + onPress: () => { + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)); + }, + subscribedSettings: [CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR], + shouldHide: reimbursable !== CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT || !reimbursableExpenseReportDefaultVendor, + }, + ], + shouldHide: false, + shouldExpand: isAccordionExpanded, + shouldAnimateSection: shouldAnimateAccordionSection, }, ]; @@ -96,21 +123,17 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyConnectionsProp wrapperStyle={[styles.mv3, styles.ph5]} /> ); - default: + case 'accordion': return ( - - - + {item.children.map((child) => renderDefault(child))} + ); + default: + return renderDefault(item); } })} diff --git a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx index af85a65a39c9..67203416f497 100644 --- a/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx +++ b/src/pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage.tsx @@ -1,5 +1,7 @@ import {Str} from 'expensify-common'; -import React from 'react'; +import React, {useEffect, useState} from 'react'; +import {useSharedValue} from 'react-native-reanimated'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -51,10 +53,23 @@ function SageIntacctToggleMappingsPage({route}: SageIntacctToggleMappingsPagePro const policy = usePolicy(route.params.policyID); const mappingName: SageIntacctMappingName = route.params.mapping; const policyID: string = policy?.id ?? '-1'; - const config = policy?.connections?.intacct?.config; - const translationKeys = getDisplayTypeTranslationKeys(config?.mappings?.[mappingName]); const isImportMappingEnable = config?.mappings?.[mappingName] !== CONST.SAGE_INTACCT_MAPPING_VALUE.NONE; + const isAccordionExpanded = useSharedValue(isImportMappingEnable); + const shouldAnimateAccordionSection = useSharedValue(false); + + // We are storing translation keys in the local state for animation purposes. + // Otherwise, the values change to undefined immediately after clicking, before the closing animation finishes, + // resulting in a janky animation effect. + const [translationKeys, setTranslationKey] = useState(undefined); + + useEffect(() => { + if (!isImportMappingEnable) { + return; + } + setTranslationKey(getDisplayTypeTranslationKeys(config?.mappings?.[mappingName])); + }, [isImportMappingEnable, config?.mappings, mappingName]); + return ( { const mappingValue = enabled ? CONST.SAGE_INTACCT_MAPPING_VALUE.TAG : CONST.SAGE_INTACCT_MAPPING_VALUE.NONE; updateSageIntacctMappingValue(policyID, mappingName, mappingValue, config?.mappings?.[mappingName]); + isAccordionExpanded.set(enabled); + shouldAnimateAccordionSection.set(true); }} pendingAction={settingsPendingAction([mappingName], config?.pendingFields)} errors={ErrorUtils.getLatestErrorField(config ?? {}, mappingName)} onCloseError={() => clearSageIntacctErrorField(policyID, mappingName)} /> - {isImportMappingEnable && ( + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_MAPPINGS_TYPE.getRoute(policyID, mappingName))} brickRoadIndicator={areSettingsInErrorFields([mappingName], config?.errorFields) ? 'error' : undefined} + hintText={translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined} /> - - {translationKeys?.descriptionKey ? translate(translationKeys?.descriptionKey) : undefined} - - )} + ); } diff --git a/src/pages/workspace/accounting/intacct/types.ts b/src/pages/workspace/accounting/intacct/types.ts index 79883a0c9104..4ad6348054f3 100644 --- a/src/pages/workspace/accounting/intacct/types.ts +++ b/src/pages/workspace/accounting/intacct/types.ts @@ -1,7 +1,19 @@ +import type {SharedValue} from 'react-native-reanimated'; import type {MenuItemProps} from '@components/MenuItem'; import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; import type {ToggleSettingOptionRowProps} from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; + +type ToggleItemWithKey = ToggleItem & {key: string}; + +type ExtendedMenuItemWithSubscribedSettings = MenuItemToRender | ToggleItemWithKey | AccordionItem; + +type MenuItemToRender = MenuItemWithSubscribedSettings & { + /** Optional hint text passed to the MenuItemWithTopDescription */ + hintText?: string; +}; + type MenuItem = MenuItemProps & { /** Type of the item */ type: 'menuitem'; @@ -27,4 +39,21 @@ type ToggleItem = ToggleSettingOptionRowProps & { shouldHide?: boolean; }; -export type {MenuItem, ToggleItem}; +type AccordionItem = { + /** Type of the item */ + type: 'accordion'; + + /** Items nested inside the accordion */ + children: MenuItemToRender[]; + + /** Whether the item should be hidden */ + shouldHide: boolean; + + /** Indicates if the accordion is expanded */ + shouldExpand: SharedValue; + + /** Indicates if the accordion opening and closing should be animated */ + shouldAnimateSection: SharedValue; +}; + +export type {MenuItem, ToggleItem, MenuItemWithSubscribedSettings, AccordionItem, ExtendedMenuItemWithSubscribedSettings, MenuItemToRender}; diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx index d6a56ae823aa..5d7df8b7a876 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAdvancedPage.tsx @@ -1,6 +1,8 @@ import {CONST as COMMON_CONST} from 'expensify-common'; import React, {useMemo} from 'react'; import {View} from 'react-native'; +import {useSharedValue} from 'react-native-reanimated'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -24,7 +26,7 @@ import { getFilteredReimbursableAccountOptions, settingsPendingAction, } from '@libs/PolicyUtils'; -import type {DividerLineItem, MenuItem, ToggleItem} from '@pages/workspace/accounting/netsuite/types'; +import type {ExtendedMenuItemWithSubscribedSettings, MenuItemToRender} from '@pages/workspace/accounting/netsuite/types'; import { shouldHideCustomFormIDOptions, shouldHideExportJournalsTo, @@ -39,8 +41,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; -type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; - function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -51,6 +51,9 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { const accountingMethod = policy?.connections?.netsuite?.options?.config?.accountingMethod; const {payableList} = policy?.connections?.netsuite?.options?.data ?? {}; + const shouldShowCustomFormIDOptions = useSharedValue(!shouldHideCustomFormIDOptions(config)); + const shouldAnimateAccordionSection = useSharedValue(false); + const selectedReimbursementAccount = useMemo( () => findSelectedBankAccountWithDefaultSelect(getFilteredReimbursableAccountOptions(payableList), config?.reimbursementAccountID), [payableList, config?.reimbursementAccountID], @@ -69,7 +72,25 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { return findSelectedBankAccountWithDefaultSelect(getFilteredApprovalAccountOptions(payableList), config?.approvalAccount); }, [config?.approvalAccount, payableList, translate]); - const menuItems: Array = [ + const renderDefaultMenuItem = (item: MenuItemToRender) => { + return ( + + + + ); + }; + + const menuItems: ExtendedMenuItemWithSubscribedSettings[] = [ { type: 'menuitem', title: autoSyncConfig?.autoSync?.enabled ? translate('common.enabled') : translate('common.disabled'), @@ -207,25 +228,37 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { switchAccessibilityLabel: translate('workspace.netsuite.advancedConfig.customFormIDDescription'), shouldPlaceSubtitleBelowSwitch: true, onCloseError: () => clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_ENABLED), - onToggle: (isEnabled) => updateNetSuiteCustomFormIDOptionsEnabled(policyID, isEnabled), + onToggle: (isEnabled) => { + updateNetSuiteCustomFormIDOptionsEnabled(policyID, isEnabled); + shouldShowCustomFormIDOptions.set(isEnabled); + shouldAnimateAccordionSection.set(true); + }, pendingAction: settingsPendingAction([CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_ENABLED], config?.pendingFields), errors: getLatestErrorField(config, CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_ENABLED), }, { - type: 'menuitem', - description: translate('workspace.netsuite.advancedConfig.customFormIDReimbursable'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE)), - title: config?.customFormIDOptions?.reimbursable?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.reimbursableExpensesExportDestination]], - subscribedSettings: [CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_TYPE.REIMBURSABLE], - shouldHide: shouldHideCustomFormIDOptions(config), - }, - { - type: 'menuitem', - description: translate('workspace.netsuite.advancedConfig.customFormIDNonReimbursable'), - onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)), - title: config?.customFormIDOptions?.nonReimbursable?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]], - subscribedSettings: [CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_TYPE.NON_REIMBURSABLE], - shouldHide: shouldHideCustomFormIDOptions(config), + type: 'accordion', + children: [ + { + type: 'menuitem', + description: translate('workspace.netsuite.advancedConfig.customFormIDReimbursable'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.REIMBURSABLE)), + title: config?.customFormIDOptions?.reimbursable?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.reimbursableExpensesExportDestination]], + subscribedSettings: [CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_TYPE.REIMBURSABLE], + shouldHide: shouldHideCustomFormIDOptions(config), + }, + { + type: 'menuitem', + description: translate('workspace.netsuite.advancedConfig.customFormIDNonReimbursable'), + onPress: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_CUSTOM_FORM_ID.getRoute(policyID, CONST.NETSUITE_EXPENSE_TYPE.NON_REIMBURSABLE)), + title: config?.customFormIDOptions?.nonReimbursable?.[CONST.NETSUITE_MAP_EXPORT_DESTINATION[config.nonreimbursableExpensesExportDestination]], + subscribedSettings: [CONST.NETSUITE_CONFIG.CUSTOM_FORM_ID_TYPE.NON_REIMBURSABLE], + shouldHide: shouldHideCustomFormIDOptions(config), + }, + ], + shouldHide: false, + shouldExpand: shouldShowCustomFormIDOptions, + shouldAnimateSection: shouldAnimateAccordionSection, }, ]; @@ -243,7 +276,7 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { connectionName={CONST.POLICY.CONNECTIONS.NAME.NETSUITE} > {menuItems - .filter((item) => !item.shouldHide) + .filter((item) => !item?.shouldHide) .map((item) => { switch (item.type) { case 'divider': @@ -264,24 +297,19 @@ function NetSuiteAdvancedPage({policy}: WithPolicyConnectionsProps) { wrapperStyle={[styles.mv3, styles.ph5]} /> ); - default: + case 'accordion': return ( - - - + {item.children.map((child) => { + return renderDefaultMenuItem(child); + })} + ); + default: + return renderDefaultMenuItem(item); } })} diff --git a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAutoSyncPage.tsx b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAutoSyncPage.tsx index 992b42dd9478..2836b3d60075 100644 --- a/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAutoSyncPage.tsx +++ b/src/pages/workspace/accounting/netsuite/advanced/NetSuiteAutoSyncPage.tsx @@ -1,9 +1,11 @@ import {CONST as COMMON_CONST} from 'expensify-common'; import React from 'react'; +import Accordion from '@components/Accordion'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Connections from '@libs/actions/connections/NetSuiteCommands'; @@ -29,6 +31,8 @@ function NetSuiteAutoSyncPage({policy, route}: WithPolicyConnectionsProps) { const pendingAction = settingsPendingAction([CONST.NETSUITE_CONFIG.AUTO_SYNC], autoSyncConfig?.pendingFields) ?? settingsPendingAction([CONST.NETSUITE_CONFIG.ACCOUNTING_METHOD], config?.pendingFields); + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!autoSyncConfig?.autoSync?.enabled); + return ( - {!!autoSyncConfig?.autoSync?.enabled && ( + + Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NETSUITE_ACCOUNTING_METHOD.getRoute(policyID))} /> - )} + ); diff --git a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx index bb94ca59e630..895ed6b63705 100644 --- a/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx +++ b/src/pages/workspace/accounting/netsuite/import/NetSuiteImportCustomersOrProjectsPage.tsx @@ -1,9 +1,11 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import RenderHTML from '@components/RenderHTML'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateNetSuiteCrossSubsidiaryCustomersConfiguration, updateNetSuiteImportMapping} from '@libs/actions/connections/NetSuiteCommands'; @@ -31,6 +33,7 @@ function NetSuiteImportCustomersOrProjectsPage({policy}: WithPolicyConnectionsPr const importCustomer = importMappings?.customers ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT; const importJobs = importMappings?.jobs ?? CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT; const importedValue = importMappings?.customers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT ? importCustomer : importJobs; + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(importedValue !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT); const updateMapping = useCallback( (importField: ImportField, isEnabled: boolean) => { @@ -95,7 +98,10 @@ function NetSuiteImportCustomersOrProjectsPage({policy}: WithPolicyConnectionsPr errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.CUSTOMER_MAPPINGS.JOBS)} onCloseError={() => Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.CUSTOMER_MAPPINGS.JOBS)} /> - {importedValue !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT && ( + Policy.clearNetSuiteErrorField(policyID, CONST.NETSUITE_CONFIG.SYNC_OPTIONS.CROSS_SUBSIDIARY_CUSTOMERS)} /> - )} - {importedValue !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NETSUITE_DEFAULT && ( - )} + ); } diff --git a/src/pages/workspace/accounting/netsuite/types.ts b/src/pages/workspace/accounting/netsuite/types.ts index 1bb872d3dd92..f4b1a885c667 100644 --- a/src/pages/workspace/accounting/netsuite/types.ts +++ b/src/pages/workspace/accounting/netsuite/types.ts @@ -1,3 +1,4 @@ +import type {SharedValue} from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import type {MenuItemProps} from '@components/MenuItem'; import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback'; @@ -9,6 +10,15 @@ import type {NetSuiteCustomFieldForm} from '@src/types/form'; import type {Policy} from '@src/types/onyx'; import type {NetSuiteCustomList, NetSuiteCustomSegment} from '@src/types/onyx/Policy'; +type MenuItemWithSubscribedSettings = Pick & {subscribedSettings?: string[]}; + +type MenuItemToRender = MenuItemWithSubscribedSettings & { + /** Optional hint text passed to the MenuItemWithTopDescription */ + hintText?: string; +}; + +type ExtendedMenuItemWithSubscribedSettings = MenuItemToRender | ToggleItem | DividerLineItem | AccordionItem; + type MenuItem = MenuItemProps & { /** Type of the item */ type: 'menuitem'; @@ -45,6 +55,23 @@ type ToggleItem = ToggleSettingOptionRowProps & { shouldHide?: boolean; }; +type AccordionItem = { + /** Type of the item */ + type: 'accordion'; + + /** Items nested inside the accordion */ + children: MenuItemToRender[]; + + /** Whether the item should be hidden */ + shouldHide: boolean; + + /** Indicates if the accordion is expanded */ + shouldExpand: SharedValue; + + /** Indicates if the accordion opening and closing should be animated */ + shouldAnimateSection: SharedValue; +}; + type ExpenseRouteParams = { expenseType: ValueOf; }; @@ -78,4 +105,14 @@ type CustomListSelectorType = SelectorType & { id: string; }; -export type {MenuItem, DividerLineItem, ToggleItem, ExpenseRouteParams, CustomFieldSubStepWithPolicy, CustomListSelectorType}; +export type { + MenuItem, + MenuItemToRender, + DividerLineItem, + ToggleItem, + AccordionItem, + ExpenseRouteParams, + CustomFieldSubStepWithPolicy, + CustomListSelectorType, + ExtendedMenuItemWithSubscribedSettings, +}; diff --git a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountPage.tsx b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountPage.tsx index 3c4d781804fb..75ad8a86eff5 100644 --- a/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountPage.tsx +++ b/src/pages/workspace/accounting/qbd/export/QuickbooksDesktopCompanyCardExpenseAccountPage.tsx @@ -1,7 +1,9 @@ import React, {useMemo} from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop'; @@ -34,6 +36,8 @@ function QuickbooksDesktopCompanyCardExpenseAccountPage({policy}: WithPolicyConn return qbdReimbursableAccounts.find(({id}) => nonReimbursableAccount === id)?.name || qbdReimbursableAccounts.at(0)?.name || translate('workspace.qbd.notConfigured'); }, [policy?.connections?.quickbooksDesktop, nonReimbursable, translate, nonReimbursableAccount]); + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!qbdConfig?.shouldAutoCreateVendor); + const sections = [ { title: nonReimbursable ? translate(`workspace.qbd.accounts.${nonReimbursable}`) : undefined, @@ -96,7 +100,11 @@ function QuickbooksDesktopCompanyCardExpenseAccountPage({policy}: WithPolicyConn onToggle={(isOn) => QuickbooksDesktop.updateQuickbooksDesktopShouldAutoCreateVendor(policyID, isOn)} onCloseError={() => clearQBDErrorField(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.SHOULD_AUTO_CREATE_VENDOR)} /> - {!!qbdConfig?.shouldAutoCreateVendor && ( + + - )} + )} diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx index 6ce56c44c680..4a9d459cf4bd 100644 --- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx +++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopClassesPage.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop'; @@ -23,6 +25,8 @@ function QuickbooksDesktopClassesPage({policy}: WithPolicyProps) { const isSwitchOn = !!(qbdConfig?.mappings?.classes && qbdConfig.mappings.classes !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isReportFieldsSelected = qbdConfig?.mappings?.classes === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(isSwitchOn); + return ( clearQBDErrorField(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CLASSES)} /> - {isSwitchOn && ( + - )} + ); } diff --git a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx index 3f0300dc2bf4..3f7a9f1606c4 100644 --- a/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx +++ b/src/pages/workspace/accounting/qbd/import/QuickbooksDesktopCustomersPage.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as QuickbooksDesktop from '@libs/actions/connections/QuickbooksDesktop'; @@ -23,6 +25,8 @@ function QuickbooksDesktopCustomersPage({policy}: WithPolicyProps) { const isSwitchOn = !!(qbdConfig?.mappings?.customers && qbdConfig.mappings.customers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isReportFieldsSelected = qbdConfig?.mappings?.customers === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(isSwitchOn); + return ( clearQBDErrorField(policyID, CONST.QUICKBOOKS_DESKTOP_CONFIG.MAPPINGS.CUSTOMERS)} /> - {isSwitchOn && ( + - )} + ); } diff --git a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx index f01d35c8a349..14be50461112 100644 --- a/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx +++ b/src/pages/workspace/accounting/qbo/advanced/QuickbooksAdvancedPage.tsx @@ -1,8 +1,10 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; @@ -47,7 +49,9 @@ function QuickbooksAdvancedPage({policy}: WithPolicyConnectionsProps) { const autoCreateVendorConst = CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR; const defaultVendorConst = CONST.QUICKBOOKS_CONFIG.NON_REIMBURSABLE_BILL_DEFAULT_VENDOR; - const sectionMenuItems = [ + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(isSyncReimbursedSwitchOn); + + const AccordionMenuItems = [ { title: selectedQboAccountName, description: translate('workspace.qbo.advancedConfig.qboBillPaymentAccount'), @@ -68,7 +72,7 @@ function QuickbooksAdvancedPage({policy}: WithPolicyConnectionsProps) { const syncReimbursedSubMenuItems = () => ( - {sectionMenuItems.map((item) => ( + {AccordionMenuItems.map((item) => ( clearQBOErrorField(policyID, item.subscribedSetting)} /> ))} - {isSyncReimbursedSwitchOn && syncReimbursedSubMenuItems()} + + {syncReimbursedSubMenuItems()} + ); } diff --git a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx index 00f46060c126..ec0a235ad540 100644 --- a/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx +++ b/src/pages/workspace/accounting/qbo/export/QuickbooksCompanyCardExpenseAccountPage.tsx @@ -1,7 +1,9 @@ import React from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateManyPolicyConnectionConfigs} from '@libs/actions/connections'; @@ -24,6 +26,8 @@ function QuickbooksCompanyCardExpenseAccountPage({policy}: WithPolicyConnections const {vendors} = policy?.connections?.quickbooksOnline?.data ?? {}; const nonReimbursableBillDefaultVendorObject = vendors?.find((vendor) => vendor.id === qboConfig?.nonReimbursableBillDefaultVendor); + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!qboConfig?.autoCreateVendor); + const sections = [ { title: qboConfig?.nonReimbursableExpensesExportDestination ? translate(`workspace.qbo.accounts.${qboConfig?.nonReimbursableExpensesExportDestination}`) : undefined, @@ -73,7 +77,6 @@ function QuickbooksCompanyCardExpenseAccountPage({policy}: WithPolicyConnections switchAccessibilityLabel={translate('workspace.qbo.defaultVendorDescription')} wrapperStyle={[styles.ph5, styles.mb3, styles.mt1]} isActive={!!qboConfig?.autoCreateVendor} - shouldPlaceSubtitleBelowSwitch pendingAction={settingsPendingAction([CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR], qboConfig?.pendingFields)} errors={getLatestErrorField(qboConfig, CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR)} onToggle={(isOn) => @@ -94,7 +97,10 @@ function QuickbooksCompanyCardExpenseAccountPage({policy}: WithPolicyConnections } onCloseError={() => clearQBOErrorField(policyID, CONST.QUICKBOOKS_CONFIG.AUTO_CREATE_VENDOR)} /> - {qboConfig?.autoCreateVendor && ( + - )} + )} diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx index ae72999ad975..c436f332a8c0 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksClassesPage.tsx @@ -1,11 +1,13 @@ import React from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as QuickbooksOnline from '@libs/actions/connections/QuickbooksOnline'; -import * as ErrorUtils from '@libs/ErrorUtils'; +import {updateQuickbooksOnlineSyncClasses} from '@libs/actions/connections/QuickbooksOnline'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {areSettingsInErrorFields, settingsPendingAction} from '@libs/PolicyUtils'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; @@ -23,6 +25,8 @@ function QuickbooksClassesPage({policy}: WithPolicyProps) { const isSwitchOn = !!(qboConfig?.syncClasses && qboConfig.syncClasses !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isReportFieldsSelected = qboConfig?.syncClasses === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(isSwitchOn); + return ( - QuickbooksOnline.updateQuickbooksOnlineSyncClasses( - policyID, - isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, - qboConfig?.syncClasses, - ) + updateQuickbooksOnlineSyncClasses(policyID, isSwitchOn ? CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE : CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG, qboConfig?.syncClasses) } pendingAction={settingsPendingAction([CONST.QUICKBOOKS_CONFIG.SYNC_CLASSES], qboConfig?.pendingFields)} - errors={ErrorUtils.getLatestErrorField(qboConfig, CONST.QUICKBOOKS_CONFIG.SYNC_CLASSES)} + errors={getLatestErrorField(qboConfig, CONST.QUICKBOOKS_CONFIG.SYNC_CLASSES)} onCloseError={() => clearQBOErrorField(policyID, CONST.QUICKBOOKS_CONFIG.SYNC_CLASSES)} /> - {isSwitchOn && ( + - )} + ); } diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx index 8b842e737de6..551849415198 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksCustomersPage.tsx @@ -1,4 +1,6 @@ -import React from 'react'; +import React, {useEffect} from 'react'; +import {useSharedValue} from 'react-native-reanimated'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -22,6 +24,20 @@ function QuickbooksCustomersPage({policy}: WithPolicyProps) { const qboConfig = policy?.connections?.quickbooksOnline?.config; const isSwitchOn = !!(qboConfig?.syncCustomers && qboConfig?.syncCustomers !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isReportFieldsSelected = qboConfig?.syncCustomers === CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD; + + const isAccordionExpanded = useSharedValue(isSwitchOn); + const shouldAnimateAccordionSection = useSharedValue(false); + const hasMounted = useSharedValue(false); + + useEffect(() => { + isAccordionExpanded.set(isSwitchOn); + if (hasMounted.get()) { + shouldAnimateAccordionSection.set(true); + } else { + hasMounted.set(true); + } + }, [hasMounted, isAccordionExpanded, isSwitchOn, shouldAnimateAccordionSection]); + return ( clearQBOErrorField(policyID, CONST.QUICKBOOKS_CONFIG.SYNC_CUSTOMERS)} /> - {isSwitchOn && ( + + - )} + ); } diff --git a/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx b/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx index 3f405f8a3f8f..26916ca207ce 100644 --- a/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx +++ b/src/pages/workspace/accounting/qbo/import/QuickbooksLocationsPage.tsx @@ -1,9 +1,9 @@ import React, {useCallback, useEffect} from 'react'; -import {View} from 'react-native'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import Text from '@components/Text'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as QuickbooksOnline from '@libs/actions/connections/QuickbooksOnline'; @@ -27,6 +27,7 @@ function QuickbooksLocationsPage({policy}: WithPolicyProps) { const isSwitchOn = !!(qboConfig?.syncLocations && qboConfig?.syncLocations !== CONST.INTEGRATION_ENTITY_MAP_TYPES.NONE); const isTagsSelected = qboConfig?.syncLocations === CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG; const shouldShowLineItemsRestriction = shouldShowLocationsLineItemsRestriction(qboConfig); + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(isSwitchOn); const updateQuickbooksOnlineSyncLocations = useCallback( (settingValue: IntegrationEntityMap) => { @@ -78,7 +79,10 @@ function QuickbooksLocationsPage({policy}: WithPolicyProps) { onCloseError={() => clearQBOErrorField(policyID, CONST.QUICKBOOKS_CONFIG.SYNC_LOCATIONS)} pendingAction={PolicyUtils.settingsPendingAction([CONST.QUICKBOOKS_CONFIG.SYNC_LOCATIONS], qboConfig?.pendingFields)} /> - {isSwitchOn && ( + - )} - - {shouldShowLineItemsRestriction && isSwitchOn && ( - - {translate('workspace.qbo.locationsLineItemsRestrictionDescription')} - - )} + ); } diff --git a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx index 91096880c938..898b4dc326bd 100644 --- a/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/XeroTrackingCategoryConfigurationPage.tsx @@ -1,8 +1,10 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Xero from '@libs/actions/connections/Xero'; @@ -26,6 +28,8 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { const xeroConfig = policy?.connections?.xero?.config; const isSwitchOn = !!xeroConfig?.importTrackingCategories; + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!xeroConfig?.importTrackingCategories); + const menuItems = useMemo(() => { const trackingCategories = Xero.getTrackingCategories(policy); return trackingCategories.map((category: XeroTrackingCategory & {value: string}) => ({ @@ -61,7 +65,10 @@ function XeroTrackingCategoryConfigurationPage({policy}: WithPolicyProps) { errors={ErrorUtils.getLatestErrorField(xeroConfig ?? {}, CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES)} onCloseError={() => Policy.clearXeroErrorField(policyID, CONST.XERO_CONFIG.IMPORT_TRACKING_CATEGORIES)} /> - {!!xeroConfig?.importTrackingCategories && ( + {menuItems.map((menuItem) => ( ))} - )} + ); } diff --git a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx index bcf24942047d..bb49e2a160ea 100644 --- a/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx +++ b/src/pages/workspace/accounting/xero/advanced/XeroAdvancedPage.tsx @@ -1,7 +1,9 @@ import React, {useMemo} from 'react'; +import Accordion from '@components/Accordion'; import ConnectionLayout from '@components/ConnectionLayout'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import useAccordionAnimation from '@hooks/useAccordionAnimation'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -39,6 +41,8 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) { const currentXeroOrganizationName = useMemo(() => getCurrentXeroOrganizationName(policy ?? undefined), [policy]); + const {isAccordionExpanded, shouldAnimateAccordionSection} = useAccordionAnimation(!!sync?.syncReimbursedReports); + return ( Policy.clearXeroErrorField(policyID, CONST.XERO_CONFIG.SYNC_REIMBURSED_REPORTS)} /> - {!!sync?.syncReimbursedReports && ( + <> - )} + ); } diff --git a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx index 25e9af97ebba..1d5c51642af5 100644 --- a/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx +++ b/src/pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage.tsx @@ -33,6 +33,7 @@ function XeroCustomerConfigurationPage({policy}: WithPolicyProps) { { - isExpanded.set(isActive); - }, [isExpanded, isActive]); + isAccordionExpanded.set(isActive); + }, [isAccordionExpanded, isActive]); const subtitleHtml = useMemo(() => { if (!subtitle || !shouldParseSubtitle || typeof subtitle !== 'string') { @@ -184,7 +183,7 @@ function ToggleSettingOptionRow({ disabledAction={disabledAction} accessibilityLabel={switchAccessibilityLabel} onToggle={(isOn) => { - isToggleTriggered.set(true); + shouldAnimateAccordionSection.set(true); onToggle(isOn); }} isOn={isActive} @@ -194,9 +193,9 @@ function ToggleSettingOptionRow({ {shouldPlaceSubtitleBelowSwitch && subtitle && subTitleView} {subMenuItems}