[Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page#94060
[Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page#94060fedirjh wants to merge 13 commits into
Conversation
…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.
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
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 --helpTypically, you'd want to translate only what you changed by running |
…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.
There was a problem hiding this comment.
💡 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".
…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).
|
@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] |
|
|
||
| const controlOnlyFeatures = formatList( | ||
| getControlOnlySelectedParts(targetPolicies, parts).map((part) => translate(FEATURE_ROWS.find((row) => row.part === part)?.labelKey ?? 'workspace.common.rules')), | ||
| ); |
There was a problem hiding this comment.
❌ 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.
Explanation of Change
Release 2 of the Add Bulk Workspace Edits project: the upgrade-flow work on the App side for copying workspace settings.
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.
New Upgrade RHP step inserted between Select Features and Confirm. It is shown only when both:
canPolicyAccessFeature()), andOtherwise the flow goes straight to Confirm.
New screen/route/page —
POLICY_COPY_SETTINGS.UPGRADE→policy/:policyID/copy-settings/upgrade→CopyPolicySettingsUpgradePage, registered in the modal stack, linking config, andWORKSPACES_LIST_TO_RHP.Confirmation action — the page's primary button calls
UpgradeToCorporatewith the IDs of all selected Collect workspaces, passingpolicyIDList(bulk) instead ofpolicyID. The sharedUpgradeConfirmationsuccess 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
UpgradeIntrointo a reusableUpgradeIntroView.Fixed Issues
$ #88674
PROPOSAL: N/A (internal task)
Tests
Offline tests
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
CleanShot.2026-06-19.at.19.49.57.mp4
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari