[Hold App/89963] [Bulk workspace edits] Add Copy Policy Settings select-workspaces page (step 1 of 3)#90079
[Hold App/89963] [Bulk workspace edits] Add Copy Policy Settings select-workspaces page (step 1 of 3)#90079fedirjh wants to merge 20 commits intoExpensify:mainfrom
Conversation
Adds the failure RBR message used when a bulk Copy Policy Settings request fails. Defined across all ten language files so the TranslationPaths type accepts it.
Implements the Onyx + API logic for the bulk Copy Policy Settings flow. Exports: - setCopyPolicySettingsData / clearCopyPolicySettings — Onyx helpers - requestCopyPolicySettingsNotification — fires CopyPolicySettings_Notify - PARTS_TO_POLICY_FIELDS — part key → list of Policy field names; drives pendingFields expansion so OfflineWithFeedback wrappers grey out per-tab during the copy - buildCopyPolicySettingsData — produces optimistic/success/failure Onyx updates following the four-step algorithm in the design doc: field patch from source, per-target merge with snapshot rollback, SET-level overwrite of POLICY_CATEGORIES / POLICY_TAGS collection keys, and currentStep lifecycle on COPY_POLICY_SETTINGS - copyPolicySettings — entry point that calls API.write customUnits handling preserves each target's existing distance / per-diem unit ID and only generates a new hex ID when the target has no unit of that type yet.
Covers the four behaviors required by the design doc: - each part key in isolation patches the right Policy fields and marks each one pending - SET-level overwrite of POLICY_CATEGORIES and POLICY_TAGS collection keys, with failure data restoring the target's snapshot - failure data restores the target's pre-copy field values - customUnits preservation: target's existing distance / per-diem unit ID is reused; a new ID is generated only when absent
Switches from `import type * as OnyxCommon` to `import type {Errors}`
to satisfy the no-restricted-syntax rule that disallows namespace
imports from sibling modules.
Replaces `import * as` namespace imports for `@libs/API`, `@libs/ErrorUtils`, and `@libs/NumberUtils` with named imports (`write`, `getMicroSecondOnyxErrorWithTranslationKey`, `generateHexadecimalValue`) to satisfy the no-restricted-syntax rule that disallows namespace imports from `@libs`. Also picks up a prettier formatting fix in the test import.
Renames the placeholder custom unit IDs (`SOURCEDISTID`,
`SOURCEPERDIEM`, `TARGETDISTID`, `TARGETPERDIEMID`) to digit-only
13-character strings so cspell stops flagging them as unknown words.
The strings remain valid `[0-9A-F]{13}` hex IDs and tests still pass.
Includes a JSDoc comment to clarify the purpose of the sourcePolicyID field, indicating that it represents the ID of the source policy from which settings are copied.
Adds title, selectWorkspaces, whichWorkspaces, searchPlaceholder, and selectAll keys for the new "Copy settings" RHP flow's first step, across all ten language files.
Adds a new "Copy settings" item next to "Duplicate Workspace" in the admin three-dot menu on WorkspacesListPage. Uses the Gear icon and navigates to ROUTES.POLICY_COPY_SETTINGS, the first step of the new RHP flow. Visible only for admins (existing isAdmin guard).
First RHP step of the bulk Copy Policy Settings flow. Renders a SelectionList with MultiSelectListItem of policies the current user is admin of, excluding: - the source policy - personal policies - policies where the user is not admin - when the source is Corporate, non-Corporate targets (Release 1 restriction; Issue 7 lifts this with an upgrade step) Layout follows the design doc: - select-all Checkbox above the list (indeterminate when partial) - SearchBar appears only when there are more than 12 eligible workspaces - "Next" persists targetPolicyIDs through setCopyPolicySettingsData and routes to POLICY_COPY_SETTINGS_SELECT_FEATURES; disabled until at least one workspace is selected Adds CONST.SENTRY_LABEL.WORKSPACE.COPY_SETTINGS_SELECT_WORKSPACES_SELECT_ALL for the select-all pressable.
|
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 |
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.
|
Per review on PR Expensify#90079: use the Plus icon for "Duplicate workspace" and the Copy (duplicate) icon for "Copy settings", to better match each item's intent.
Per review on PR Expensify#90079: MultiSelectListItem renders its checkbox on the right side, so move the select-all Checkbox to the right of the header row (label on the left, checkbox on the right) so they line up with the per-row checkboxes below.
Drops the separate SearchBar component above the list in favour of SelectionList's built-in textInputOptions, which matches the search field used in the New Chat / start chat flow at /new/chat. The built-in input owns its own margin/padding so the design is consistent across the app. The "more than 12 eligible workspaces" gate is preserved by toggling shouldShowTextInput.
The built-in select-all header always rendered its checkbox on the left, which misaligned with lists whose per-row checkboxes use selectionButtonPosition=RIGHT (e.g. MultiSelectListItem). Adds a selectionButtonPosition prop to ListHeader (default LEFT for existing callers) and threads it from BaseSelectionList. When set to RIGHT, the label/checkbox order is swapped and the row uses justifyContentBetween so the checkbox sits flush right, matching the list rows below.
Three review fixes on the Select Workspaces page: - Use SelectionList's built-in onSelectAll with selectionButtonPosition=RIGHT, so the search input sits above the select-all header and the select-all checkbox lines up with the per-row checkboxes (both on the right). Drops the custom select-all header that used to sit above the SelectionList. - Render the workspace avatar at AVATAR_SIZE.DEFAULT via a leftElement on each list item. The previous icons-based path inside MultiSelectListItem hardcodes AVATAR_SIZE.SMALLER, which looked too small. Falls back to getDefaultWorkspaceAvatar when avatarURL is missing. - Track selectedTargetIDs in local useState instead of writing to COPY_POLICY_SETTINGS on every toggle. The Onyx key is now updated only when the user confirms "Next", so closing the RHP mid-flow naturally drops the in-progress selection without needing a manual clear-on-mount.
The page now uses SelectionList's built-in select-all header, which already reuses workspace.people.selectAll for its label and SENTRY_LABEL.SELECTION_LIST.LIST_HEADER_SELECT_ALL for its pressable. The custom workspace.copySettings.selectAll key (10 language files) and SENTRY_LABEL.WORKSPACE.COPY_SETTINGS_SELECT_WORKSPACES_SELECT_ALL are no longer referenced — remove them.
Explanation of Change
Adds the entry point and the first RHP step of the new "Copy settings" flow that's part of the Bulk workspace edits project. This is task 3 of the project — task 1 (scaffold #89959) is merged; task 2 (action layer #89963) is still open and this PR is stacked on top of #89963. Please land #89963 first.
What this PR adds
Three-dot menu entry in
src/pages/workspace/WorkspacesListPage.tsx— a new "Copy settings" item with the Gear icon next to "Duplicate Workspace", visible only for admins (existingisAdminguard). Navigates toROUTES.POLICY_COPY_SETTINGS.Select Workspaces page in
src/pages/workspace/copyPolicySettings/CopyPolicySettingsSelectWorkspacesPage.tsx— first step of the RHP flow. Renders aSelectionListwithMultiSelectListItemof policies the current user is admin of, with the eligibility rules from the design doc:CORPORATE, onlyCORPORATEtargets are shown (Release 1 restriction; Issue 7 lifts this in R2 with an upgrade step)Layout:
<Checkbox>above the list (indeterminate when partial)<SearchBar>appears only when there are more than 12 eligible workspacestargetPolicyIDsviasetCopyPolicySettingsData(introduced in [No QA] [Bulk workspace edits] Add CopyPolicySettings action file and tests #89963) and navigates toPOLICY_COPY_SETTINGS_SELECT_FEATURES; disabled until at least one workspace is selectedTranslations — new
workspace.copySettings.{title, selectWorkspaces, whichWorkspaces, searchPlaceholder, selectAll}block in all 10 language files.Sentry label —
CONST.SENTRY_LABEL.WORKSPACE.COPY_SETTINGS_SELECT_WORKSPACES_SELECT_ALLfor the select-all pressable.The page reads its persisted state (
targetPolicyIDs) directly fromONYXKEYS.COPY_POLICY_SETTINGSso re-entry restores the user's previous selection.Fixed Issues
$ #88669
PROPOSAL: N/A
Tests
Setup
Sign in as an admin on at least two non-personal workspaces (a mix of Team/Collect and Corporate/Control if possible).
Test 1 — Three-dot menu entry
Test 2 — Eligibility filtering
Test 3 — Workspace avatars
avatarURL, verify the default workspace avatar (initials/colored placeholder) is rendered.Test 4 — Select-all alignment and behavior
Test 5 — Search
Test 6 — Next / commit selection
copyPolicySettings.targetPolicyIDsnow contains the selected IDs andcopyPolicySettings.sourcePolicyIDis set.Test 7 — Selection cleared on RHP close
Test 8 — Console
Offline tests
The page is a pure read of
ONYXKEYS.COLLECTION.POLICYplus auseStateselection; the only Onyx write happens at "Next" viasetCopyPolicySettingsData. Both should work fully offline:copyPolicySettings.targetPolicyIDsis written to Onyx even while offline.QA Steps
Same as Tests 1–8 above on the staging environment.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, 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.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari