Skip to content

[Bulk workspace edits] Add Copy Policy Settings select-features page (step 2 of 3)#90220

Merged
yuwenmemon merged 48 commits into
Expensify:mainfrom
fedirjh:copy-policy-settings-page-2
May 26, 2026
Merged

[Bulk workspace edits] Add Copy Policy Settings select-features page (step 2 of 3)#90220
yuwenmemon merged 48 commits into
Expensify:mainfrom
fedirjh:copy-policy-settings-page-2

Conversation

@fedirjh

@fedirjh fedirjh commented May 11, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Implements the second RHP step of the new Copy Settings flow that's part of the Bulk workspace edits project — task 4 of the project. Earlier tasks: scaffold #89959 (merged), action layer #89963, page 1 #90079. This PR is stacked on top of #90079 and depends on tasks 1 & 2 — please land the upstream PRs first.

What this PR adds

  1. Accounting-compatibility utility in src/libs/CopyPolicySettingsUtils.ts. Ports Policy::getConnectionCompanyID from Auth/auth/lib/Policy.cpp to TypeScript so the App can compare external accounts across policies:

    • QBO → config.realmId
    • NetSuite → top-level accountID
    • Xero / Sage Intacct / QuickBooks Desktop / Certinia → config.credentials.companyID

    Exposes arePoliciesAccountingCompatible(source, target) — two policies are compatible only when both have no accounting connection, or they share the same connection name AND a matching non-empty companyID. areAllTargetsAccountingCompatible(source, targets) runs that across every target.

  2. Unit tests in tests/unit/CopyPolicySettingsUtilsTest.ts — 15 cases covering the design-doc scenarios:

    • source connected to nothing, target connected to NetSuite → INCOMPATIBLE
    • source connected to QBO, target connected to nothing → INCOMPATIBLE
    • source NetSuite Acme Corp vs target NetSuite ExpensivePie → INCOMPATIBLE
    • different connection names entirely → INCOMPATIBLE
    • both unconnected → COMPATIBLE
    • same connection + same companyID → COMPATIBLE
    • missing companyID on either side → INCOMPATIBLE
    • QBO realmId equality
    • per-integration getConnectionCompanyID extraction
  3. Select Features page in src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectFeaturesPage.tsx. Renders the 13 part rows (overview, members, reports, accounting, categories, tags, taxes, workflows, rules, distanceRates, perDiem, invoices, travel) in a SelectionList with MultiSelectListItem. Behaviors:

    • Disabled accounting-dependent rows. categories, tags, reports, taxes are greyed out + uncheckable whenever any selected target has a mismatched (or missing) connection vs. the source.
    • Force-enable on Accounting. Selecting accounting flips those same four parts to isSelected: true, isDisabled: true so they can't be unchecked (syncing a connection implies its coding must come along).
    • Workflows-without-members warning. On "Next", if workflows is selected but members is not, opens a confirm modal explaining that Submission and Payment settings will be copied without members. On confirm (or when there's no conflict) persists parts to ONYXKEYS.COPY_POLICY_SETTINGS via setCopyPolicySettingsData and routes to POLICY_COPY_SETTINGS_CONFIRM.
    • Local selection state. Closing the RHP discards the in-progress selection (same pattern as Page 1).
  4. Translations — new workspace.copyPolicySettings.{selectFeatures, whichFeatures, accountingDisabledTooltip, workflowsWithoutMembersTitle, workflowsWithoutMembersPrompt} across all 10 language files.

Fixed Issues

$ #88671
PROPOSAL: N/A

Tests

Setup

Sign in as an admin on at least three Corporate (Control) workspaces. Use a workspace with multiple members, categories, tags, workflows, and optionally an accounting connection as the source — this ensures all applicable feature rows appear. Have at least one pair of workspaces sharing the same accounting connection (e.g. both connected to the same NetSuite account) and at least one workspace connected to a different accounting account (or none).

Test 1 — Page renders only applicable feature rows

  1. From the Workspaces list three-dot menu, open Copy settings → Select workspaces → tick at least one target → Next.
  2. Verify the Select Features step shows only feature rows that are relevant to the source workspace. The full set of possible rows is: Overview, Members, Reports, Accounting, Categories, Tags, Taxes, Workflows, Rules, Distance rates, Per Diem, Invoices, Travel.
  3. Features are conditionally shown based on the source policy's configuration:
    • Members only appears when the source workspace has more than 1 member.
    • Reports only appears when the source has report fields.
    • Accounting only appears when the source has an active accounting connection.
    • Categories only appears when the source has categories.
    • Tags only appears when the source has tags.
    • Taxes only appears when the source has taxes configured.
    • Workflows only appears when the source has workflow rules configured.
    • Rules only appears on Corporate policies with workspace rules.
    • Distance rates only appears when the source has distance rates enabled with rates configured.
    • Per Diem only appears when the source has per diem rates.
    • Invoices only appears when the source has invoices enabled with invoice configuration.
    • Travel only appears when the source has travel enabled.
    • Overview always appears.

Test 2 — Accounting-compatibility disables Categories/Tags/Reports/Taxes/Accounting

  1. From a source workspace connected to e.g. NetSuite account A, pick targets that all share the same NetSuite account A. Reach Select Features.
  2. Verify Categories, Tags, Reports, Taxes, Accounting are toggleable (not greyed out).
  3. Go back and add a target connected to a different NetSuite account (or no accounting connection).
  4. Verify those rows are now greyed out and cannot be selected, with a tooltip explaining the accounting mismatch.
  5. Repeat with the design-doc scenarios:
    • source connected to nothing, target connected to NetSuite → disabled
    • source connected to QBO, target connected to nothing → disabled
    • source NetSuite Acme Corp vs target NetSuite ExpensivePie → disabled

Test 3 — Force-enable on Accounting

  1. Reach Select Features with accounting-compatible targets.
  2. Toggle Accounting on.
  3. Verify Categories, Tags, Reports, Taxes become checked and locked (cannot be toggled off).
  4. Toggle Accounting off.
  5. Verify those four rows return to their normal toggleable state and revert to their previous (toggleable) selection.

Test 4 — Workflows-without-members warning

  1. Reach Select Features with a source workspace that has >1 member.
  2. Select Workflows but leave Members unchecked.
  3. Click "Next".
  4. Verify a confirm modal appears with the explanatory prompt about copying workflows without members.
  5. Click "Cancel" → verify the modal closes and you remain on the page with selection intact.
  6. Click "Next" again, confirm — verify the flow advances to the Confirm step and copyPolicySettings.parts in Onyx contains ["workflows", ...].
  7. Repeat with both Workflows and Members selected → verify no modal appears and the flow advances immediately.
  8. Repeat with a source workspace that has only 1 member (the owner) — Members row won't be visible. Select Workflows and click Next → verify the modal does NOT appear (there are no members to copy, so the warning is irrelevant).

Test 5 — Select-all + Next

  1. Reach Select Features with accounting-compatible targets.
  2. Tap "Select all" → verify all visible rows become checked.
  3. Tap "Select all" again → verify all clear.
  4. Reach the step again, select nothing → verify "Next" is disabled.
  5. Select at least one row → "Next" becomes enabled.
  6. Click "Next" → verify copyPolicySettings.parts is written to Onyx and the flow routes to the Confirm step.

Test 6 — Selection cleared on RHP close

  1. Reach Select Features, select 2–3 parts.
  2. Close the RHP via X/back without clicking "Next".
  3. Re-enter the flow and reach Select Features.
  4. Verify the previous part selection is not restored.

Test 7 — Console

  • Verify that no errors appear in the JS console while running tests 1–6.

Offline tests

The page only reads from Onyx (COLLECTION.POLICY + COPY_POLICY_SETTINGS) and writes via setCopyPolicySettingsData on Next. Both should work offline:

  1. Toggle off network.
  2. Reach Select Features.
  3. Verify the accounting-disabled rows are correctly computed from the cached policy data.
  4. Force-enable / select-all / Next still work; parts is written to Onyx.

QA Steps

Same as Tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Screenshot 2026-05-20 at 11 29 59 AM Screenshot 2026-05-20 at 11 30 05 AM Screenshot 2026-05-20 at 11 30 12 AM
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

fedirjh added 4 commits May 11, 2026 17:28
Ports Auth's Policy::getConnectionCompanyID logic (QBO → config.realmId,
NetSuite → top-level accountID, Xero/Intacct/QBD/Certinia →
config.credentials.companyID) to TypeScript.

Exposes:
- getConnectionCompanyID(policy, connectionName)
- getAccountingConnectionIdentity(policy) — first valid connection
- arePoliciesAccountingCompatible(source, target) — true only when both
  policies have no accounting connection, or share the same connection
  AND a matching non-empty companyID
- areAllTargetsAccountingCompatible(source, targets) — every-target
  compatibility check used by the Select Features step

Page 2 will use this to disable Categories/Tags/Reports/Taxes whenever
any selected target has a mismatched (or missing) connection.
Covers the design-doc scenarios for accounting compatibility:
- source connected to nothing, target connected to NetSuite
- source connected to QBO, target connected to nothing
- source NetSuite Acme vs target NetSuite ExpensivePie
- different connection names entirely
- both unconnected
- same connection + same companyID
- missing companyID on either side
- QBO realmId equality

Plus per-integration getConnectionCompanyID extraction (QBO realmId,
NetSuite top-level accountID, credentials.companyID for Sage Intacct,
Xero, and QBD).
Adds the strings used by the Select Features step (Page 2 of the
Copy Policy Settings flow) to workspace.copyPolicySettings across all
ten language files:

- selectFeatures — page H2
- whichFeatures — page subheader
- accountingDisabledTooltip — tooltip for the
  Categories/Tags/Reports/Taxes rows when targets are not connected
  to the same accounting account
- workflowsWithoutMembersTitle / workflowsWithoutMembersPrompt —
  confirm modal shown when the admin selects Workflows but not
  Members
Second RHP step of the bulk Copy Policy Settings flow. Renders the
13 part rows (overview, members, reports, accounting, categories,
tags, taxes, workflows, rules, distanceRates, perDiem, invoices,
travel) in a SelectionList with MultiSelectListItem.

Behaviors required by the design doc:
- Categories/Tags/Reports/Taxes are disabled when any selected target
  workspace has a mismatched (or missing) accounting connection
  relative to the source, via areAllTargetsAccountingCompatible.
- Selecting "Accounting" force-selects + force-disables those same
  four parts (syncing a connection implies its coding must come
  along).
- On "Next", if Workflows is selected but Members is not, opens a
  confirm modal warning that Submission and Payment settings will be
  copied without members. On confirm (or when there's no conflict)
  persists `parts` to COPY_POLICY_SETTINGS via setCopyPolicySettingsData
  and routes to POLICY_COPY_SETTINGS_CONFIRM.
- Selection lives in useState so closing the RHP discards it (matches
  Page 1).
@melvin-bot

melvin-bot Bot commented May 11, 2026

Copy link
Copy Markdown

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running:

npx ts-node ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx ts-node ./scripts/generateTranslations.ts --compare-ref main

@fedirjh fedirjh changed the title Add Copy Policy Settings select-features page (step 2 of 3) [Bulk workspace edits] Add Copy Policy Settings select-features page (step 2 of 3) May 11, 2026
@codecov

codecov Bot commented May 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/components/SelectionList/BaseSelectionList.tsx 67.98% <100.00%> (+0.77%) ⬆️
...components/SelectionList/components/ListHeader.tsx 90.90% <100.00%> (+5.19%) ⬆️
...ettings/CopyPolicySettingsSelectWorkspacesPage.tsx 0.00% <0.00%> (ø)
src/libs/CopyPolicySettingsUtils.ts 76.76% <76.76%> (ø)
...ySettings/CopyPolicySettingsSelectFeaturesPage.tsx 0.00% <0.00%> (ø)
... and 10 files with indirect coverage changes

fedirjh added 12 commits May 11, 2026 19:15
Replaces the verbose "Which configurations do you want to copy to the
other workspaces? Only these settings will be overwritten on the
target workspaces." with the shorter, more direct:

"Select the settings to overwrite on your existing workspaces."

Localizes the new copy across all ten language files.
cspell doesn't recognize "togglable" as a dictionary word. Rename the
local variables (togglableParts / togglableSet / togglable comment) to
the more conventional "selectable" — the meaning is identical in this
context and it passes cspell without dictionary additions.
Refactors the page to inline the computed values (effectiveSelectedFeatures,
listItems, isFeatureDisabled, selectableFeatures, toggleAll, onConfirm)
that were previously wrapped in useMemo / useCallback. The values are
already cheap to compute and the wrappers added noise without
measurable benefit. Renames selectedParts -> selectedFeatures /
togglePart -> toggleFeature for consistency with the page's UI
vocabulary.
When the select-all checkbox sits on the right, the label is followed
by the checkbox immediately, so the trailing horizontal padding from
`styles.ph3` collides with the checkbox. Switch to `styles.pr3` on
the right-positioned variant so only leading padding is applied to
the label, keeping spacing symmetric with the left-positioned layout.
Mirrors the WorkspaceDuplicateSelectFeaturesForm pattern: each row now
includes a short summary derived from the source workspace ("12
categories", "3 members", "QuickBooks Online", "$, Berlin, …" for
overview, etc.) so the admin can see at a glance what will be copied.

Reuses existing helpers (getMemberAccountIDsForWorkspace,
getReportFieldsByPolicyID, getAllValidConnectedIntegration,
getDistanceRateCustomUnit / getPerDiemCustomUnit, getWorkflowRules /
getWorkspaceRules from the duplicate flow, formatAddressToString).
Filters out pending-delete entries when counting so the numbers reflect
what would actually be copied. Bumps alternateNumberOfSupportedLines
to 2 since some rows (rules, invoices) can produce longer captions.
@fedirjh fedirjh requested review from francoisl and ishpaul777 May 20, 2026 11:36
@fedirjh fedirjh marked this pull request as ready for review May 20, 2026 11:36
@fedirjh fedirjh requested review from a team as code owners May 20, 2026 11:36
@melvin-bot

melvin-bot Bot commented May 20, 2026

Copy link
Copy Markdown

@truph01 Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot Bot removed request for a team May 20, 2026 11:36
@ishpaul777

Copy link
Copy Markdown
Contributor

@yuwenmemon All yours!

@yuwenmemon yuwenmemon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Francois is on parental leave so I'll be reviewing for this project in his stead

@yuwenmemon yuwenmemon merged commit b68c197 into Expensify:main May 26, 2026
45 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚧 @yuwenmemon has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@fedirjh fedirjh deleted the copy-policy-settings-page-2 branch May 26, 2026 19:10
@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/yuwenmemon in version: 9.3.83-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91791 was identified to be related to this PR.

@mitarachim

Copy link
Copy Markdown

Deploy Blocker #91792 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91793 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91805 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91821 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91825 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91827 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91830 was identified to be related to this PR.

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #91833 was identified to be related to this PR.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.3.83-3 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants