Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
0fd5e18
Add workspace.copySettings translation block
fedirjh May 8, 2026
585f827
Add Copy settings entry to workspace three-dot menu
fedirjh May 8, 2026
5b03b95
Implement Copy settings select-workspaces page
fedirjh May 8, 2026
8e76652
Address review: swap Duplicate/Copy menu icons
fedirjh May 8, 2026
ff4acc4
Address review: align select-all checkbox with row checkboxes
fedirjh May 8, 2026
b1184c8
Address review: use SelectionList built-in text input for search
fedirjh May 8, 2026
6f4b833
Make SelectionList ListHeader respect selectionButtonPosition
fedirjh May 8, 2026
5bbf0a1
Address review: built-in select-all, avatars, local selection state
fedirjh May 8, 2026
ebdd219
Drop unused selectAll translation key and sentry label
fedirjh May 8, 2026
726b296
Merge branch 'Expensify:main' into copy-policy-settings-page-1
fedirjh May 11, 2026
5f2d85f
Merge branch 'Expensify:main' into copy-policy-settings-page-1
fedirjh May 11, 2026
e075919
Merge copySettings translations into copyPolicySettings
fedirjh May 11, 2026
2cb71bc
Repoint Copy Settings UI to consolidated translation keys
fedirjh May 11, 2026
af3dca1
Address review: toggle select-all against visible filtered rows
fedirjh May 11, 2026
c34ecd5
Merge branch 'main' into copy-policy-settings-page-1
fedirjh May 19, 2026
2398319
Merge remote-tracking branch 'upstream' into copy-policy-settings-page-1
fedirjh May 19, 2026
c051272
Merge remote-tracking branch 'upstream' into copy-policy-settings-page-1
fedirjh May 20, 2026
b9fd1de
feat(i18n): add copy policy settings page translations
fedirjh May 20, 2026
86223f1
Merge branch 'Expensify:main' into copy-policy-settings-page-1
fedirjh May 21, 2026
942a7fc
fix: hide Copy Settings menu when no eligible target workspaces exist
fedirjh May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/SelectionList/BaseSelectionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ function BaseSelectionList<TItem extends ListItem>({
shouldShowSelectAllButton={!!onSelectAll}
shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow}
selectAllAccessibilityLabel={selectAllAccessibilityLabel}
selectionButtonPosition={selectionButtonPosition}
/>
);

Expand Down
69 changes: 45 additions & 24 deletions src/components/SelectionList/components/ListHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import type {StyleProp, TextStyle, ViewStyle} from 'react-native';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import Checkbox from '@components/Checkbox';
import {PressableWithFeedback} from '@components/Pressable';
import type {DataDetailsType, ListItem} from '@components/SelectionList/types';
Expand Down Expand Up @@ -36,6 +37,9 @@ type ListHeaderProps<TItem extends ListItem> = {

/** Custom accessibility label for the select all checkbox, providing context about what is being selected */
selectAllAccessibilityLabel?: string;

/** Side on which the select-all checkbox should be rendered, to align with the per-row checkboxes */
selectionButtonPosition?: ValueOf<typeof CONST.SELECTION_BUTTON_POSITION>;
};

function ListHeader<TItem extends ListItem>({
Expand All @@ -48,6 +52,7 @@ function ListHeader<TItem extends ListItem>({
shouldShowSelectAllButton,
shouldPreventDefaultFocusOnSelectRow,
selectAllAccessibilityLabel,
selectionButtonPosition = CONST.SELECTION_BUTTON_POSITION.LEFT,
}: ListHeaderProps<TItem>) {
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand All @@ -65,35 +70,51 @@ function ListHeader<TItem extends ListItem>({
e.preventDefault();
};

const checkbox = (
<Checkbox
testID="selection-list-select-all-checkbox"
accessibilityLabel={selectAllAccessibilityLabel ?? translate('accessibilityHints.selectAllItems')}
isChecked={dataDetails.allSelected}
isIndeterminate={dataDetails.someSelected}
onPress={onSelectAll}
disabled={allDisabled}
/>
);

const label = !customListHeader && (
<PressableWithFeedback
style={[styles.userSelectNone, styles.flexRow, styles.alignItemsCenter]}
onPress={onSelectAll}
accessibilityLabel={selectAllAccessibilityLabel ?? translate('accessibilityHints.selectAllItems')}
sentryLabel={CONST.SENTRY_LABEL.SELECTION_LIST.LIST_HEADER_SELECT_ALL}
accessibilityRole="button"
accessibilityState={{checked: dataDetails.allSelected, disabled: allDisabled}}
disabled={allDisabled}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={handleMouseDown}
>
<Text style={[styles.textStrong, styles.ph3, selectAllTextStyle]}>{translate('workspace.people.selectAll')}</Text>
</PressableWithFeedback>
);

const isCheckboxOnRight = selectionButtonPosition === CONST.SELECTION_BUTTON_POSITION.RIGHT;

return (
<View
style={[styles.userSelectNone, styles.peopleRow, styles.ph5, styles.pb3, headerStyle, styles.selectionListStickyHeader]}
accessibilityRole={CONST.ROLE.HEADER}
>
<View style={[styles.flexRow, styles.alignItemsCenter]}>
<Checkbox
testID="selection-list-select-all-checkbox"
accessibilityLabel={selectAllAccessibilityLabel ?? translate('accessibilityHints.selectAllItems')}
isChecked={dataDetails.allSelected}
isIndeterminate={dataDetails.someSelected}
onPress={onSelectAll}
disabled={allDisabled}
/>

{!customListHeader && (
<PressableWithFeedback
style={[styles.userSelectNone, styles.flexRow, styles.alignItemsCenter]}
onPress={onSelectAll}
accessibilityLabel={selectAllAccessibilityLabel ?? translate('accessibilityHints.selectAllItems')}
sentryLabel={CONST.SENTRY_LABEL.SELECTION_LIST.LIST_HEADER_SELECT_ALL}
accessibilityRole="button"
accessibilityState={{checked: dataDetails.allSelected, disabled: allDisabled}}
disabled={allDisabled}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true}}
onMouseDown={handleMouseDown}
>
<Text style={[styles.textStrong, styles.ph3, selectAllTextStyle]}>{translate('workspace.people.selectAll')}</Text>
</PressableWithFeedback>
<View style={[styles.flexRow, styles.alignItemsCenter, isCheckboxOnRight && styles.flex1, isCheckboxOnRight && styles.justifyContentBetween]}>
{isCheckboxOnRight ? (
<>
{label}
{checkbox}
</>
) : (
<>
{checkbox}
{label}
</>
)}
</View>
{customListHeader}
Expand Down
8 changes: 7 additions & 1 deletion src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6014,7 +6014,13 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
`Sie sind dabei, ${newWorkspaceName ?? ''} mit ${totalMembers ?? 0} Mitgliedern aus dem ursprünglichen Workspace zu erstellen und zu teilen.`,
error: 'Beim Duplizieren deines neuen Workspace ist ein Fehler aufgetreten. Bitte versuche es erneut.',
},
copyPolicySettings: {error: 'Beim Kopieren der Arbeitsbereichseinstellungen ist ein Fehler aufgetreten. Bitte versuche es erneut.'},
copyPolicySettings: {
error: 'Beim Kopieren der Arbeitsbereichseinstellungen ist ein Fehler aufgetreten. Bitte versuche es erneut.',
title: 'Einstellungen kopieren',
selectWorkspaces: 'Arbeitsbereiche auswählen',
description: 'Wählen Sie die Arbeitsbereiche aus, in die Sie Einstellungen kopieren möchten, und wählen Sie dann die Einstellungen aus, die Sie kopieren möchten.',
searchPlaceholder: 'Workspaces durchsuchen',
},
emptyWorkspace: {
title: 'Du hast keine Arbeitsbereiche',
subtitle: 'Belege erfassen, Auslagen erstatten, Reisen verwalten, Rechnungen versenden und mehr.',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6036,6 +6036,10 @@ const translations = {
},
copyPolicySettings: {
error: 'An error occurred while copying workspace settings. Please try again.',
title: 'Copy settings',
selectWorkspaces: 'Select workspaces',
description: 'Choose the workspaces you want to copy settings to, then select the settings you’d like to copy.',
searchPlaceholder: 'Search workspaces',
},
emptyWorkspace: {
title: 'No workspaces yet',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5851,6 +5851,10 @@ ${amount} para ${merchant} - ${date}`,
},
copyPolicySettings: {
error: 'Se produjo un error al copiar la configuración del espacio de trabajo. Por favor, inténtalo de nuevo.',
title: 'Copiar configuración',
selectWorkspaces: 'Selecciona espacios de trabajo',
description: 'Elige los espacios de trabajo a los que quieres copiar la configuración y luego selecciona los ajustes que quieras copiar.',
searchPlaceholder: 'Buscar espacios de trabajo',
},
emptyWorkspace: {
title: 'Aún no hay espacios de trabajo',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6034,7 +6034,13 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
`Vous êtes sur le point de créer et de partager ${newWorkspaceName ?? ''} avec ${totalMembers ?? 0} membres de l’espace de travail d’origine.`,
error: 'Une erreur s’est produite lors de la duplication de votre nouvel espace de travail. Veuillez réessayer.',
},
copyPolicySettings: {error: 'Une erreur s’est produite lors de la copie des paramètres de l’espace de travail. Veuillez réessayer.'},
copyPolicySettings: {
error: 'Une erreur s’est produite lors de la copie des paramètres de l’espace de travail. Veuillez réessayer.',
title: 'Copier les paramètres',
selectWorkspaces: 'Sélectionner des espaces de travail',
description: 'Choisissez les espaces de travail vers lesquels vous souhaitez copier les paramètres, puis sélectionnez les paramètres que vous souhaitez copier.',
searchPlaceholder: 'Rechercher des espaces de travail',
},
emptyWorkspace: {
title: 'Vous n’avez aucun espace de travail',
subtitle: 'Suivez les reçus, remboursez les dépenses, gérez les voyages, envoyez des factures, et plus encore.',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6004,7 +6004,13 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
`Stai per creare e condividere ${newWorkspaceName ?? ''} con ${totalMembers ?? 0} membri dello spazio di lavoro originale.`,
error: 'Si è verificato un errore durante la duplicazione del tuo nuovo workspace. Riprova.',
},
copyPolicySettings: {error: 'Si è verificato un errore durante la copia delle impostazioni dello spazio di lavoro. Riprova.'},
copyPolicySettings: {
error: 'Si è verificato un errore durante la copia delle impostazioni dello spazio di lavoro. Riprova.',
title: 'Copia impostazioni',
selectWorkspaces: 'Seleziona gli spazi di lavoro',
description: 'Scegli gli spazi di lavoro a cui vuoi copiare le impostazioni, poi seleziona le impostazioni che desideri copiare.',
searchPlaceholder: 'Cerca spazio di lavoro',
},
emptyWorkspace: {
title: 'Non hai nessuna area di lavoro',
subtitle: 'Tieni traccia delle ricevute, rimborsa le spese, gestisci i viaggi, invia le fatture e altro ancora.',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5936,7 +5936,13 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO
`元のワークスペースから ${totalMembers ?? 0} 人のメンバーと一緒に、${newWorkspaceName ?? ''} を作成して共有しようとしています。`,
error: '新しいワークスペースの複製中にエラーが発生しました。もう一度お試しください。',
},
copyPolicySettings: {error: 'ワークスペース設定のコピー中にエラーが発生しました。もう一度お試しください。'},
copyPolicySettings: {
error: 'ワークスペース設定のコピー中にエラーが発生しました。もう一度お試しください。',
title: '設定をコピー',
selectWorkspaces: 'ワークスペースを選択',
description: '設定をコピーしたいワークスペースを選択し、その後、コピーしたい設定を選びます。',
searchPlaceholder: 'ワークスペースを検索',
},
emptyWorkspace: {
title: 'ワークスペースがありません',
subtitle: '領収書を管理し、経費を精算し、出張を管理し、請求書を送信するなど、さまざまなことができます。',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5983,7 +5983,13 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_
`Je staat op het punt ${newWorkspaceName ?? ''} te maken en te delen met ${totalMembers ?? 0} leden van de oorspronkelijke werkruimte.`,
error: 'Er is een fout opgetreden bij het dupliceren van je nieuwe werkruimte. Probeer het opnieuw.',
},
copyPolicySettings: {error: 'Er is een fout opgetreden bij het kopiëren van de werkruimtainstellingen. Probeer het opnieuw.'},
copyPolicySettings: {
error: 'Er is een fout opgetreden bij het kopiëren van de werkruimtainstellingen. Probeer het opnieuw.',
title: 'Instellingen kopiëren',
selectWorkspaces: 'Selecteer werkruimtes',
description: 'Kies de werkruimtes waarnaar je instellingen wilt kopiëren en selecteer daarna de instellingen die je wilt kopiëren.',
searchPlaceholder: 'Werkruimtes zoeken',
},
emptyWorkspace: {
title: 'Je hebt geen werkruimtes',
subtitle: 'Volg bonnen, vergoed uitgaven, beheer reizen, verstuur facturen en meer.',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5977,7 +5977,13 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
`Za chwilę utworzysz i udostępnisz ${newWorkspaceName ?? ''} ${totalMembers ?? 0} członkom oryginalnego obszaru roboczego.`,
error: 'Wystąpił błąd podczas duplikowania Twojego nowego obszaru roboczego. Spróbuj ponownie.',
},
copyPolicySettings: {error: 'Wystąpił błąd podczas kopiowania ustawień przestrzeni roboczej. Spróbuj ponownie.'},
copyPolicySettings: {
error: 'Wystąpił błąd podczas kopiowania ustawień przestrzeni roboczej. Spróbuj ponownie.',
title: 'Skopiuj ustawienia',
selectWorkspaces: 'Wybierz przestrzenie robocze',
description: 'Wybierz przestrzenie robocze, do których chcesz skopiować ustawienia, a potem zaznacz ustawienia, które chcesz skopiować.',
searchPlaceholder: 'Szukaj przestrzeni roboczych',
},
emptyWorkspace: {
title: 'Nie masz żadnych przestrzeni roboczych',
subtitle: 'Śledź paragony, rozliczaj wydatki, zarządzaj podróżami, wysyłaj faktury i wiele więcej.',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5983,7 +5983,13 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS
`Você está prestes to criar e compartilhar ${newWorkspaceName ?? ''} com ${totalMembers ?? 0} membros do workspace original.`,
error: 'Ocorreu um erro ao duplicar seu novo espaço de trabalho. Tente novamente.',
},
copyPolicySettings: {error: 'Ocorreu um erro ao copiar as configurações do workspace. Tente novamente.'},
copyPolicySettings: {
error: 'Ocorreu um erro ao copiar as configurações do workspace. Tente novamente.',
title: 'Copiar configurações',
selectWorkspaces: 'Selecionar espaços de trabalho',
description: 'Escolha os espaços de trabalho para os quais você quer copiar as configurações e, em seguida, selecione quais configurações deseja copiar.',
searchPlaceholder: 'Buscar espaços de trabalho',
},
emptyWorkspace: {
title: 'Você não tem nenhum workspace',
subtitle: 'Controle recibos, reembolse despesas, gerencie viagens, envie faturas e muito mais.',
Expand Down
8 changes: 7 additions & 1 deletion src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5831,7 +5831,13 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
`您即将创建并共享 ${newWorkspaceName ?? ''},其中包含来自原始工作区的 ${totalMembers ?? 0} 位成员。`,
error: '复制您的新工作区时发生错误。请重试。',
},
copyPolicySettings: {error: '复制工作区设置时发生错误。请重试。'},
copyPolicySettings: {
error: '复制工作区设置时发生错误。请重试。',
title: '复制设置',
selectWorkspaces: '选择工作区',
description: '选择要复制设置到的工作区,然后选择你想复制的设置。',
searchPlaceholder: '搜索工作区',
},
emptyWorkspace: {
title: '你还没有工作区',
subtitle: '跟踪收据、报销费用、管理差旅、发送发票等。',
Expand Down
35 changes: 32 additions & 3 deletions src/pages/workspace/WorkspacesListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useIsFocused, useRoute} from '@react-navigation/native';
import {Str} from 'expensify-common';
import React, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {FlatList, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
Expand Down Expand Up @@ -133,7 +133,7 @@ function isUserReimburserForPolicy(policies: Record<string, PolicyType | undefin
}

function WorkspacesListPage() {
const icons = useMemoizedLazyExpensifyIcons(['Building', 'Exit', 'Copy', 'Star', 'Trashcan', 'Transfer', 'FallbackWorkspaceAvatar']);
const icons = useMemoizedLazyExpensifyIcons(['Building', 'Exit', 'Copy', 'Plus', 'Star', 'Trashcan', 'Transfer', 'FallbackWorkspaceAvatar']);
const theme = useTheme();
const styles = useThemeStyles();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Building', 'Exit', 'Copy', 'Star', 'Trashcan', 'Transfer', 'Plus', 'FallbackWorkspaceAvatar']);
Expand Down Expand Up @@ -353,6 +353,24 @@ function WorkspacesListPage() {
);
};

const copySettingsEligibleTargets = useMemo(() => {
const adminNonPersonal: string[] = [];
const corporateOnly: string[] = [];
if (!policies) {
return {adminNonPersonal, corporateOnly};
}
for (const policy of Object.values(policies)) {
if (!policy || policy.type === CONST.POLICY.TYPE.PERSONAL || !isPolicyAdmin(policy, session?.email)) {
continue;
}
adminNonPersonal.push(policy.id);
if (policy.type === CONST.POLICY.TYPE.CORPORATE) {
corporateOnly.push(policy.id);
}
}
return {adminNonPersonal, corporateOnly};
}, [policies, session?.email]);

/**
* Gets the menu item for each workspace
*/
Expand Down Expand Up @@ -393,10 +411,21 @@ function WorkspacesListPage() {

if (isAdmin) {
threeDotsMenuItems.push({
icon: icons.Copy,
icon: icons.Plus,
text: translate('workspace.common.duplicateWorkspace'),
onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE.getRoute(item.policyID)) : undefined),
});
const isSourceCorporate = item.type === CONST.POLICY.TYPE.CORPORATE;
const candidates = isSourceCorporate ? copySettingsEligibleTargets.corporateOnly : copySettingsEligibleTargets.adminNonPersonal;
const hasEligibleCopyTarget = candidates.length > 1 || (candidates.length === 1 && candidates.at(0) !== item.policyID);

if (hasEligibleCopyTarget) {
threeDotsMenuItems.push({
icon: icons.Copy,
text: translate('workspace.copyPolicySettings.title'),
onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.POLICY_COPY_SETTINGS.getRoute(item.policyID)) : undefined),
});
}
}

if (!isDefault && !item?.isJoinRequestPending && !isRestrictedToPreferredPolicy) {
Expand Down
Loading
Loading