Expensify / App

Welcome to New Expensify: a complete re-imagination of financial collaboration, centered around chat. Help us build the next generation of Expensify by sharing feedback and contributing to the code.
https://new.expensify.com
MIT License
3.1k stars 2.6k forks source link

[#Wave-Control: Add NetSuite] Settings Configuration in NewDot: Import #43437

Open yuwenmemon opened 3 weeks ago

yuwenmemon commented 3 weeks ago

Tracking Issue: https://github.com/Expensify/Expensify/issues/377671

Design Doc Section: https://docs.google.com/document/d/1WubNv_VAv78IxG4FKsi9aS0pWESfWvUYbqBAAy5b4bc/edit#heading=h.959vyh4x5v9l


(High-Level Section)

Routes

  1. Route: /settings/workspaces/{policyID}/accounting/netsuite/import

    • PageComponent: NetSuiteImportPage
  2. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/{field}

    • PageComponent: NetSuiteImportMappingPage
    • Same layout where the field is one of: departments, classes, locations, customers, jobs.
Key AP Command Payload Key Value (one of)
departments UpdateNetSuiteDepartmentsMapping mapping.departments NETSUITE_DEFAULT, TAG, or REPORT_FIELD.
classes UpdateNetSuiteClassesMapping mapping.classes NETSUITE_DEFAULT, TAG, or REPORT_FIELD.
locations UpdateNetSuiteLocationsMapping mapping.locations NETSUITE_DEFAULT, TAG, or REPORT_FIELD.
customers UpdateNetSuiteCustomersMapping mapping.customers NETSUITE_DEFAULT, TAG, or REPORT_FIELD.
jobs (projects) UpdateNetSuiteJobsMapping mapping.jobs NETSUITE_DEFAULT, TAG, or REPORT_FIELD.
  1. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/custom-segments

    • PageComponent: NetSuiteImportCustomSegmentsPage
    • This route will be valid for both segments and records.
    • API Call/Parameter:
      • N/A: this just displays the currently imported Segments/Records
  2. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/custom-lists

    • PageComponent: NetSuiteImportCustomListsPage
    • API Call/Parameter:
      • N/A: this just displays the currently imported Lists
  3. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/custom-segments/edit/{internalId}

    • PageComponent: NetSuiteImportEditCustomSegmentPage
    • API Call/Parameter:
      • UpdateNetSuiteCustomSegments
      • customSegments - JSON Array of all the custom segments. Each object would consist of segmentName, internalID, scriptID, mapping. In this scenario we would replace the element of the array and always set the whole array in the API call.
  4. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/custom-lists/{transactionFieldId}

    • PageComponent: NetSuiteImportViewCustomListPage
    • API Call/Parameter:
      • UpdateNetSuiteCustomLists
      • customLists - JSON Array of all the custom lists. Each object would consist of listName, internalID, transactionFieldID, mapping. In this scenario we would replace the element of the array and always set the whole array in the API call.
  5. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/custom-segments/new

    • PageComponent: NetSuiteImportAddCustomSegmentsPage
    • API Call/Parameter:
      • UpdateNetSuiteCustomSegments
      • customSegments - JSON Array of all the custom segments. Each object would consist of segmentName, internalID, scriptID, mapping. Ideally we would append to the existing array when adding and always set the array.
  6. Route: /settings/workspaces/{policyID}/accounting/netsuite/import/custom-lists/new

    • PageComponent: NetSuiteImportAddCustomListsPage
    • API Call/Parameter:
      • UpdateNetSuiteCustomLists
      • customLists - JSON Array of all the custom lists. Each object would consist of listName, internalID, transactionFieldID, mapping. Ideally we would append to the existing array when adding and always set the array.

NetSuiteImportPage

We’ll start with the first main page for the import called NetSuiteImportPage.

We will create a NetSuiteImportPage component for this menu of options. The import settings contain several settings. We’ll divide them into three sections.

AD_4nXft6tk9OjDEwM0a_zQqoiTyfSwyo1B80JXhAQHukaFYECuWidXeTG2ubfKzP_pBJUSaARB4Y1N6t2y6rx9PKZxqo21mKLXRo7Noc34lgB7hmwp8V6XuRues

Section 1:

Section 2:

The component contains five options mentioned in the UI frame earlier, each of them wrapped with OfflineWithFeedback. The pendingAction will be the value of policy.pendingFields.netsuite[settingName] for each. Each will pull the title from the existing value in NetSuiteConfig and the description from the appropriate translation. We’ll fill this in manually:

Tapping on each setting on the Import page will push it to its own page/pane, using an onPressConfigOption function opening a generic screen: NetSuiteImportMappingPage.

This will navigate to the appropriate page below using Navigation.navigate([ROUTE]).

AD_4nXfmYtpEDcfax8WRjSWc--xa5TniXE-a9lDNItvpaOX6uiRPyDzOAkyvTUECVeQ_efWSBnBuX6Quzho3wGqZMX3CZF2CWasMlstQYw7DHOq0Iq7eyVMDq92X

Because each of the pages has a common structure, we’ll build one screen using the ConnectionLayout. Based on the selection of the menu we’ll show a helperText below the menu. For example, when Departments has Tags selected, it will show Departments will be selectable for each individual expense on an employee’s report.

To support this, we'll need to update SelectionScreen to allow showing a helperText based on the selected field. This will require a regression test on Xero/QBO screens as well though.

The following table lists down different aspects of the screens.

Route API Command Pending Field Configuration Setting to Update
/settings/workspaces/{policyID}/accounting/netsuite/import/departments UpdateNetSuiteDepartmentsMapping policy.pendingFields.departments policy.connections.netsuite.options.config.syncOptions.mapping.departments
/settings/workspaces/{policyID}/accounting/netsuite/import/classes UpdateNetSuiteClassesMapping policy.pendingFields.classes policy.connections.netsuite.options.config.syncOptions.mapping.classes
/settings/workspaces/{policyID}/accounting/netsuite/import/locations UpdateNetSuiteLocationsMapping policy.pendingFields.locations policy.connections.netsuite.options.config.syncOptions.mapping.locations
/settings/workspaces/{policyID}/accounting/netsuite/import/customers UpdateNetSuiteCustomersMapping policy.pendingFields.customers policy.connections.netsuite.options.config.syncOptions.mapping.customers
/settings/workspaces/{policyID}/accounting/netsuite/import/jobs UpdateNetSuiteJobsMapping policy.pendingFields.jobs policy.connections.netsuite.options.config.syncOptions.mapping.jobs

For each of the pages the listed options seem to be the same. We’ll use the SelectionScreen component that’ll accept the policy and settingName as two props.

We will pull the configuration options from the connections object by separating out policy.connections.netsuite.config as NetSuiteConfig. In order to make human-readable versions of the values, we’ll extend the keys in the accounting.importTypes (in the example below, raw strings are used but we’ll use keys in en.ts and es.ts as it’s the standard):

accounting: {
    importTypes: {
        [CONST.INTEGRATION_ENTITY_MAP_TYPES.NET_SUITE_DEFAULT]: 'Netsuite employee default',
        [CONST.INTEGRATION_ENTITY_MAP_TYPES.TAG]: 'Imported, displayed as tags', // exists already
    [CONST.INTEGRATION_ENTITY_MAP_TYPES.REPORT_FIELD]: 'Imported, displayed as report fields', // exists already

    }
}

The commands to be used are mentioned in the table above.

The basic code for the page will look as follows:

<ConnectionLayout
            displayName={NetSuiteImportPage.displayName}
            headerTitle="workspace.netsuite.import.title"
            headerSubtitle={currentNetsuiteSubsidiary}
            accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
            policyID={policyID}
            featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
            contentContainerStyle={[styles.pb2, styles.ph5]}
        >

     <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}>
                <View style={styles.flex1}>
                    <Text fontSize={variables.fontSizeNormal}>{translate('workspace.accounting.netsuite.expenseCategories')}</Text>
                </View>
                <View style={[styles.flex1, styles.alignItemsEnd, styles.pl3]}>
                   <ToggleSettingOptionRow
                title={translate('workspace.accounting.netsuite.expenseCategories')}
                subtitle={translate('workspace.accounting.netsuite.expenseCategoriesDescription')}
                shouldPlaceSubtitleBelowSwitch
                isActive
           disabled
            />

                </View>

         <OfflineWithFeedback>
            <MenuItemWithTopDescription
                title={NetSuiteTitles[NetSuiteConfig.departments]} // We would loop through all the 5 fields mentioned in Section 2.                description={translate('workspace.accounting.netsuite.import.departments')}
                shouldShowRightIcon
                onPress={onPressConfigOption("departments'")}
                brickRoadIndicator={indicator}
            />
    </OfflineWithFeedback>

            </View>
             <ToggleSettingOptionRow
                title={translate('workspace.accounting.netsuite.tax')}
                subtitle={translate('workspace.accounting.netsuite.importTaxesDescription')} // We could check and reuse the keys if they exist in the common translation paths.
                shouldPlaceSubtitleBelowSwitch
                isActive
           disabled
            />
            <MenuItemWithTopDescription
                title={translate('workspace.accounting.netsuite.customSegments')} 
                shouldShowRightIcon
                onPress={Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NET_SUITE_CUSTOM_SEGMENTS.getRoute(policyID))}
                brickRoadIndicator={indicator}
            />
            <MenuItemWithTopDescription
                title={translate('workspace.accounting.netsuite.customLists)}
                shouldShowRightIcon
onPress={Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NET_SUITE_CUSTOM_LISTS.getRoute(policyID))}
                brickRoadIndicator={indicator}
            />

</ConnectionLayout>
NetSuiteImportMappingPage

This is a common mapping screen that allows you to choose the options - Tag, Report, etc. for the given field. The following code implements the SelectionScreen described here. The Selection screen takes care of the access, the policy checks, header, navigation and the title. Additionally, it needs the sections prop where we pass the list of import types.

  <SelectionScreen
            policyID={policyID}
            accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
            featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
            displayName={NetSuiteDisplayedAsPage.displayName}
            sections={[{data: filteredAccountingImportTypes}]} // we filter based on the options available for NetSuite
            listItem={RadioListItem}
            onSelectRow={updateSetting}
            initiallyFocusedOptionKey={initiallyFocusedOptionKey}
            headerContent={listHeaderComponent}
            onBackButtonPress={() => Navigation.goBack()}
            title="workspace.netsuite.import.{field}.title"
        />

Section 3:

Our next set of fields consists of implementing a multi-step form like we have for Add Bank account workflow in the workspaces. The following fields are covered in this section

**- Custom Segments

Each screen type consists of three types of screens: Listing screen, Edit screen and the "Form workflow".

Listing screen:
AD_4nXeX7f9vazmKKTFs1b80V9i12CHbRwjcDoN2TCl6HrE7wFb3_KmLr1k0PpUeDNrkpYpB6IN9xQnYdSbmmm3F9D3_B0LptLC09oGxg07ruyvll4ca2Wx23KyE

AD_4nXfO-9Rq-PREYicK8fvBjWLDhYVOuhKiMZCiyFRd3WtBblt1TRmPnQinKujvIacrbyzidDGNnwz7VhC3nkSq72ZgDUZ2oPljeXQaJFyp-BW-ltCiyOw1-pJ_

Route Field to Populate Add CTA Route View Detailed Instruction Link Empty Card Content
netsuite/import/custom-segments config.syncOptions.customSegments netsuite/import/custom-segments/new View detailed instructions Title: Add a custom segment/record
Description: View detailed instructions on configuring custom segment/record.
netsuite/import/custom-lists config.syncOptions.customLists netsuite/import/custom-lists/new View detailed instructions Title: Add a custom list
Description: View detailed instructions on configuring custom list.

Form workflow:

Add generic component code structure:

This code gives us a basic structure of the root component where the form and steps are added.

function AddCustomSegmentPage({ customSegment}: CustomSegmentProps) {
    const {translate} = useLocalize();
    const styles = useThemeStyles();

    const [customSegmentsDraft] = useOnyx(ONYXKEYS.FORMS.NET_SUITE_CUSTOM_SEGMENTS_DRAFT); // CustomSegmentPropsOnyxProps will be defined in /types/onyx

    const values = useMemo(() => getSubstepValues(CUSTOM_RECORD_STEP_KEYS, customSegmentDraft), [customSegmentDraft]);
    const submit = () => {
        // API Call comes here
        Navigation.goBack(ROUTES.CUSTOM_RECORDS_LIST);
    };

    const startFrom = useMemo(() => getInitialValuesForCustomSegment(values), [values]);

    const {
        componentToRender: SubStep,
        isEditing,
        nextScreen,
        prevScreen,
        moveTo,
        screenIndex,
        goToTheLastStep,
    } = useSubStep({
        bodyContent,
        startFrom,
        onFinished: submit,
    });

    return (
        <ScreenWrapper
            includeSafeAreaPaddingBottom={false}
            testID={AddCustomSegmentPage.displayName}
        >
            <HeaderWithBackButton
                title={translate('accounting.netsuite.customSegment.addTitle')}
                onBackButtonPress={handleBackButtonPress}
            />
            <View style={[styles.ph5, styles.mb5, styles.mt3,]}>
                <InteractiveStepSubHeader
                    startStepIndex={1}
                    stepNames={CONST.NET_SUITE_CUSTOM_SEGMENTS_FORM.STEP_NAMES}
                />
            </View>
            <SubStep
                isEditing={isEditing}
                onNext={nextScreen}
                onMove={moveTo}
            />
        </ScreenWrapper>
    );
}

AddCustomSegmentPage.displayName = AddCustomSegmentPage;

export default AddCustomSegmentPage;
Substep component code structure:

Each step of the form is added as its own separate component. We could further DRY the code by plucking out only the form component so that it can be used for the edit screen.

const STEP_FIELDS = [...];

function ScriptIdStep({onNext, isEditing}: SubStepProps) {
    const {translate} = useLocalize();
    const styles = useThemeStyles();

    const [netsuiteCustomSegmentDetails] = useOnyx(ONYXKEYS.NET_SUITE_CUSTOM_SEGMENTS);

    const defaultValue = netsuiteCustomSegmentDetails?.[CUSTOM_SEGMENT_STEPS.SCRIPT_ID] ?? '';

    const handleSubmit = useCustomSegmentFormStepFormSubmit({
        fieldIds: STEP_FIELDS,
        onNext,
        shouldSaveDraft: isEditing,
    });

    return (
        <FormProvider
            formID={ONYXKEYS.FORMS.NET_SUITE_CUSTOM_SEGMENTS}
            submitButtonText={translate(isEditing ? 'common.confirm' : 'common.next')}
            validate={validate}
            onSubmit={handleSubmit}
            style={[styles.mh5, styles.flexGrow1]}
        >
            <View>
                <Text style={[styles.textHeadlineLineHeightXXL, styles.mb3]}>{translate('accounting.netsuite.customSegments.form.scriptId')}</Text>
                <View style={[styles.flex1]}>
                    <InputWrapper
                        InputComponent={TextInput}
                        inputID={CUSTOM_SEGMENT_STEPS_KEY.SCRIPT_ID}
                        label={translate('accounting.netsuite.customSegments.form.scriptIdLabel')}
                        aria-label={translate('accounting.netsuite.customSegments.form.scriptIdLabel')}
                        role={CONST.ROLE.PRESENTATION}
                        inputMode={CONST.INPUT_MODE.TEL}
                        defaultValue={defaultValue}
                        shouldSaveDraft={!isEditing}
                        containerStyles={[styles.mt6]}
                    />
                </View>

                <Text style={[styles.textSupporting]}>{translate('accounting.netsuite.customSegments.form.scriptIdInstructions')}</Text>
            </View>
        </FormProvider>
    );
}

ScriptIdStep.displayName = 'ScriptIdStep';

export default ScriptIdStep;
Single Custom segment/records/list View

This page will be different for each type - CustomSegments and CustomLists as we’re going to list down each field for the pages. We should be able to map the fields from the form based on the constants defined earlier, for ex. CUSTOM_SEGMENT_STEPS.SCRIPT_ID.

This will help us loop through the fields and display the information using MenuItemWithDescription.

 Custom segmentrecords

AD_4nXdrjGVfU_7j3YFv-fefaZXE6U1REK0IrZAvpFTLSiwyy5SZlL-kGRGh89_0KbmW68L_UbW2DkAVet4J2wcrqE4q1OOu6PgJHcMawDjzShIl_3S35jcdk_pT

// Find the current segment record from the policy object based on the ID.

const customSegmentsRecord = policy?.config?.syncOptions?.customSegments.find(segment => segment.id === currentSegmentId) 

const fields = [CUSTOM_SEGMENT_STEPS.NAME, CUSTOM_SEGMENT_STEPS.INTERNAL_ID, CUSTOM_SEGEMENTS.SCRIPT_ID, CUSTOM_SEGMENT_STEPS.DISPLAYED_AS]; // Naming could be steps or fields. We can decide during implementation

const removeSegment = useCallback(() => {
   // There is no hard delete. We only remove the record for the array and update the array with the said API command. While the API and the example mentions segments,it is essentially the same for segments/records/lists.
     const filteredCustomSegments = policy?.config?.syncOptions?.customSegments.filter(segment => segment.id === currentSegmentId); 

     Policy.updateNetSuiteCustomSegments(policy, filteredCustomRecords);

                        Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NET_SUITE_CUSTOM_SEGNTS.getRoute(policyID));
                        setIsRemoveModalOpen(false);
}, [currentSegmentId, policy?.config?.syncOptions?.customSegments]);

 <ConnectionLayout // Common component to set the ScreenWrapper, etc.
            displayName={NetsuiteCustomSegmentsItemView.displayName}
            headerTitle="workspace.netsuite.import.customSegments.editViewTitle"
            headerSubtitle={currentNetsuiteSubsidiary} // TBD
            accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
            policyID={policyID}
            featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED}
            contentContainerStyle={[styles.pb2, styles.ph5]}
        >

     <View style={[styles.flexRow, styles.mb4, styles.alignItemsCenter, styles.justifyContentBetween]}>
        { 
             fields.map(field => { // We would loop through all the fields    
return (<OfflineWithFeedback>
            <MenuItemWithTopDescription
                           title={translate(`workspace.netsuite.import.customSegments.fields.${field}`)} description={customSegmentRecord[field]}
                shouldShowRightIcon
                onPress={Navigation.navigate(ROUTES.POLICY_ACCOUNTING_NET_SUITE_CUSTOM_SEGEMENTS_EDIT.getRoute(policyID, field))}
                brickRoadIndicator={indicator}
            />
    </OfflineWithFeedback>
            </View>

            <View>
            <OptionRow // "Remove" button.
onSelectRow={() => { setIsRemovalModalOpen(true)}} // Need to verify if onSelectRow works as onPress of button.
option={{
    text: translate('common.remove'),
    icons: [{source: Expensicons.Trashcan, name: translate('common.remove'), type: 'avatar'}],
}}   
</View>

<ConfirmModal
                    title={translate('workspace.accounting.netsuite.customSegments.remove')}
                    onConfirm={removeSegment}
                    onCancel={() =>
                        setIsRemoveModalOpen(false);
                    }
                    isVisible={isRemoveModalOpen}
                    prompt={translate('workspace.accounting.netsuite.customSegments.removeDescription')}
                    confirmText={translate('common.remove')}
                    cancelText={translate('common.cancel')}
                />
</ConnectionLayout>
Upwork Automation - Do Not Edit
  • Upwork Job URL: https://www.upwork.com/jobs/~018ff86748aefd4c38
  • Upwork Job ID: 1800287682907693558
  • Last Price Increase: 2024-06-10
Issue OwnerCurrent Issue Owner: @
Issue OwnerCurrent Issue Owner: @mananjadhav
melvin-bot[bot] commented 3 weeks ago

Triggered auto assignment to @strepanier03 (NewFeature), see https://stackoverflowteams.com/c/expensify/questions/14418#:~:text=BugZero%20process%20steps%20for%20feature%20requests for more details. Please add this Feature request to a GH project, as outlined in the SO.

melvin-bot[bot] commented 3 weeks ago

:warning: It looks like this issue is labelled as a New Feature but not tied to any GitHub Project. Keep in mind that all new features should be tied to GitHub Projects in order to properly track external CAP software time :warning:

melvin-bot[bot] commented 3 weeks ago

Job added to Upwork: https://www.upwork.com/jobs/~018ff86748aefd4c38

melvin-bot[bot] commented 3 weeks ago

Current assignee @mananjadhav is eligible for the External assigner, not assigning anyone new.

melvin-bot[bot] commented 3 weeks ago

Triggered auto assignment to Design team member for new feature review - @dubielzyk-expensify (NewFeature)

strepanier03 commented 2 weeks ago

Following along. If my assignment is meant to assign a content writer I might hand off as I'm doing resource management for the august 1 release already and that's a killer on bandwidth.

melvin-bot[bot] commented 2 weeks ago

@mananjadhav, @yuwenmemon, @strepanier03, @dubielzyk-expensify Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

strepanier03 commented 1 week ago

No update.

mananjadhav commented 1 week ago

I will be working on this towards the weekend/next week after I finish the Auth screen.

strepanier03 commented 1 week ago

Thanks for sharing that update @mananjadhav.

dubielzyk-expensify commented 1 week ago

Any updates?

melvin-bot[bot] commented 3 days ago

@mananjadhav, @yuwenmemon, @strepanier03, @dubielzyk-expensify Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

mananjadhav commented 3 days ago

Working on the TokenInput. I'll start this on the weekend.