Skip to content

[Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page#94060

Open
fedirjh wants to merge 13 commits into
Expensify:mainfrom
fedirjh:fedirjh-copy-settings-upgrade-88674
Open

[Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page#94060
fedirjh wants to merge 13 commits into
Expensify:mainfrom
fedirjh:fedirjh-copy-settings-upgrade-88674

Conversation

@fedirjh

@fedirjh fedirjh commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Release 2 of the Add Bulk Workspace Edits project: the upgrade-flow work on the App side for copying workspace settings.

  1. Removed the Control-only target restriction. Copying from a Corporate (Control) workspace no longer limits targets to Corporate workspaces. Every admin-accessible, non-personal workspace is now an eligible target at both gating points — the "Copy settings" action in the workspaces list and the Select Workspaces step.

  2. New Upgrade RHP step inserted between Select Features and Confirm. It is shown only when both:

    • at least one selected feature is Control-only (decided by canPolicyAccessFeature()), and
    • at least one target is on the Collect (Team) plan.

    Otherwise the flow goes straight to Confirm.

  3. New screen/route/pagePOLICY_COPY_SETTINGS.UPGRADEpolicy/:policyID/copy-settings/upgradeCopyPolicySettingsUpgradePage, registered in the modal stack, linking config, and WORKSPACES_LIST_TO_RHP.

  4. Confirmation action — the page's primary button calls UpgradeToCorporate with the IDs of all selected Collect workspaces, passing policyIDList (bulk) instead of policyID. The shared UpgradeConfirmation success view is shown only once the upgrade settles successfully; while pending the intro stays with a loading state, and on failure it reverts for a retry.

To keep things DRY, the upgrade card was extracted from UpgradeIntro into a reusable UpgradeIntroView.

Fixed Issues

$ #88674
PROPOSAL: N/A (internal task)

Tests

  1. Sign in as an admin of a Control (Corporate) workspace and at least one Collect (Team) workspace.
  2. From the workspaces list, open the three-dot menu on the Control workspace and click Copy settings.
  3. On Select workspaces, verify the Collect workspace now appears as an eligible target (previously hidden). Select it and tap Next.
  4. On Select settings, select a Control-only feature (e.g. Rules or Per diem) and tap Next.
  5. Verify the Upgrade step is shown (heading "Some features require a Control plan", listing the Control-only features and the source workspace name).
  6. Tap Upgrade and verify the button shows a loading state while the upgrade is in progress.
  7. Verify the success view appears once the upgrade completes, with a Continue button.
  8. Tap Continue and verify you land on the Confirm step.
  9. Repeat steps 1–4 but select only non-Control features (e.g. Categories); verify the flow skips the Upgrade step and goes straight to Confirm.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline on the Upgrade step.
  2. Verify the Upgrade button is disabled while offline.

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 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)
  • 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)
  • 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.
  • 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-06-19 at 7 49 51 PM Screenshot 2026-06-19 at 7 50 46 PM
CleanShot.2026-06-19.at.19.49.57.mp4
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

fedirjh and others added 4 commits June 19, 2026 15:56
…iction

Release 2 lifts the R1 restriction that only allowed Corporate targets when
copying from a Corporate (Control) workspace. Every admin-accessible,
non-personal workspace is now an eligible copy target regardless of plan, at
both gating points: the "Copy settings" entry action in the workspaces list and
the Select Workspaces step. Copying Control-only settings onto a Collect target
is handled by the upgrade step added separately.
Add bulkUpgradeToCorporate, which upgrades several workspaces to Corporate
(Control) in a single UpgradeToCorporate call by passing policyIDList instead of
policyID, with optimistic/success/failure Onyx updates per policy. Extend
UpgradeToCorporateParams to accept either policyID or policyIDList.
…Intro

Pull the upgrade card (icon + "Upgrade to unlock" badge, title/description,
optional plan-availability copy, button, and plans & pricing note) out of
UpgradeIntro into a presentational UpgradeIntroView that takes its content via
props, and refactor UpgradeIntro to render it. The note's subscription link is
now computed inside the view so callers don't have to. Also allow
UpgradeConfirmation to override its button label so other flows can show
"Continue" instead of "Got it, thanks".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Insert an Upgrade step between Select Features and Confirm, shown only when at
least one selected feature is Control-only (per canPolicyAccessFeature) and at
least one target is on the Collect plan; otherwise the flow goes straight to
Confirm. The new CopyPolicySettingsUpgradePage reuses UpgradeIntroView, calls
bulkUpgradeToCorporate for all selected Collect targets, and shows the shared
UpgradeConfirmation success view (with a Continue button) only once the upgrade
settles successfully - staying on the intro with a loading state while pending
and reverting on failure. Registers the new screen/route in the modal stack,
linking config, and WORKSPACES_LIST_TO_RHP, adds the upgrade-detection helpers
and English copy, and covers the pending/success/failure states with tests.
@melvin-bot

melvin-bot Bot commented Jun 19, 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 added 3 commits June 19, 2026 16:04
…ream merge

Upstream moved copy-settings target eligibility into a shared selector returning
{adminNonPersonal, corporateOnly} consumed by the new WorkspaceRowThreeDotsMenu.
Reconcile that with the R2 change (no plan restriction): the selector now returns
a flat list of every admin-accessible, non-personal workspace ID, the page derives
a per-row isEligibleToCopy boolean from it, and the row menu reads that boolean
instead of receiving the targets list. Drops the now-unused corporateOnly field
and updates PolicySelectorTest accordingly.
…pgrade copy

Generated the non-English translations for the copy-settings upgrade step copy
(workspace.copyPolicySettings.upgrade) across all supported locales.
@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/SCREENS.ts 100.00% <ø> (ø)
...s/WorkspaceListTable/WorkspaceRowThreeDotsMenu.tsx 69.23% <100.00%> (-1.14%) ⬇️
...ts/Tables/WorkspaceListTable/WorkspaceTableRow.tsx 89.36% <100.00%> (ø)
src/components/Tables/WorkspaceListTable/index.tsx 71.87% <100.00%> (ø)
.../linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts 100.00% <ø> (ø)
src/libs/Navigation/linkingConfig/config.ts 76.92% <ø> (ø)
src/libs/actions/Policy/CopyPolicySettings.ts 77.41% <100.00%> (+0.14%) ⬆️
src/pages/workspace/WorkspacesListPage.tsx 69.38% <100.00%> (ø)
...ettings/CopyPolicySettingsSelectWorkspacesPage.tsx 82.81% <100.00%> (+0.45%) ⬆️
...rc/pages/workspace/upgrade/UpgradeConfirmation.tsx 77.77% <100.00%> (ø)
... and 9 more
... and 12 files with indirect coverage changes

@fedirjh

fedirjh commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 281d4e11d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/libs/CopyPolicySettingsUtils.ts
fedirjh added 6 commits June 19, 2026 16:59
…pers

Codecov flagged the new UpgradeIntroView (0% - it was mocked everywhere) and the
new copy-settings upgrade helpers as uncovered. Add a render test for
UpgradeIntroView (content, button press, disabled state, illustration variant)
and unit tests for getControlOnlySelectedParts, getCollectTargetsToUpgrade, and
shouldShowCopyPolicySettingsUpgradeStep.
…ettings targets

Removing the Control-only restriction let any non-personal workspace be a copy
target, including Submit workspaces. Submit policies fail canPolicyAccessFeature
(they aren't paid group policies), so the upgrade gate - which only inspects
Collect targets - never flagged them and the flow went straight to Confirm,
optimistically writing Control-only settings onto a Submit workspace. Limit
eligible targets to paid group workspaces (Collect/Control) in both the
workspaces-list selector and the Select Workspaces step, so the upgrade step
(Collect -> Control) fully covers every eligible target.
This commit introduces the phrase "unlock features" in various languages, enhancing the user interface for feature unlocking across the application. The new translations have been added to the respective language files, ensuring consistency and clarity for users in German, English, Spanish, French, Italian, Japanese, Dutch, Polish, Portuguese, and Simplified Chinese.
…POLICY_FIELDS

Move the copy-settings part -> PolicyFeatureName map into CopyPolicySettings.ts
next to PARTS_TO_POLICY_FIELDS (both relate a Part to Policy vocabulary) and
export it, so CopyPolicySettingsUtils imports it instead of defining its own.
Typed as Partial<Record<Part, PolicyFeatureName>> so callers index by any Part
without a cast. A doc comment explains why it stays separate from
PARTS_TO_POLICY_FIELDS (the feature toggle isn't reliably derivable from the
copied-fields list).
@fedirjh fedirjh marked this pull request as ready for review June 19, 2026 18:57
@fedirjh fedirjh requested review from a team as code owners June 19, 2026 18:57
@melvin-bot melvin-bot Bot requested review from heyjennahay and shubham1206agra and removed request for a team and heyjennahay June 19, 2026 18:57
@melvin-bot

melvin-bot Bot commented Jun 19, 2026

Copy link
Copy Markdown

@shubham1206agra 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 the request for review from a team June 19, 2026 18:57
@fedirjh fedirjh requested a review from ishpaul777 June 19, 2026 18:58

const controlOnlyFeatures = formatList(
getControlOnlySelectedParts(targetPolicies, parts).map((part) => translate(FEATURE_ROWS.find((row) => row.part === part)?.labelKey ?? 'workspace.common.rules')),
);

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.

❌ CONSISTENCY-2 (docs)

The hardcoded fallback translation key 'workspace.common.rules' is a magic string. It is not self-explanatory as a generic fallback, and it is also a latent correctness bug: if a selected part has no matching FEATURE_ROWS entry, that feature is mislabeled as "Rules" rather than reflecting the real feature. A Part should always have a label, so this arbitrary fallback hides a gap rather than handling it meaningfully.

Consider filtering out parts that have no labelKey instead of substituting an unrelated key, e.g.:

const controlOnlyFeatures = formatList(
    getControlOnlySelectedParts(targetPolicies, parts)
        .map((part) => FEATURE_ROWS.find((row) => row.part === part)?.labelKey)
        .filter((labelKey): labelKey is TranslationPaths => !!labelKey)
        .map((labelKey) => translate(labelKey)),
);

This avoids the magic key and guarantees the rendered feature names are accurate.


Reviewed at: 42b70f2 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

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.

1 participant