From 67c8d41f3d7933a6b0e53fd898f18f56f83272f1 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 9 Jul 2025 18:54:35 +0300 Subject: [PATCH 01/29] initial --- src/ONYXKEYS.ts | 7 + src/ROUTES.ts | 8 + src/SCREENS.ts | 2 + src/languages/de.ts | 10 + src/languages/en.ts | 11 ++ src/languages/es.ts | 10 + src/languages/fr.ts | 11 ++ src/languages/it.ts | 10 + src/languages/ja.ts | 10 + src/languages/nl.ts | 10 + src/languages/pl.ts | 10 + src/languages/pt-BR.ts | 10 + src/languages/zh-hans.ts | 10 + .../ModalStackNavigators/index.tsx | 8 +- .../Navigators/RightModalNavigator.tsx | 4 + src/libs/Navigation/linkingConfig/config.ts | 6 + src/libs/Navigation/types.ts | 11 ++ src/libs/actions/Policy/Policy.ts | 12 ++ src/pages/workspace/WorkspacesListPage.tsx | 63 ++++--- .../duplicate/WorkspaceDuplicateForm.tsx | 174 ++++++++++++++++++ .../duplicate/WorkspaceDuplicatePage.tsx | 33 ++++ .../WorkspaceDuplicateSelectFeaturesForm.tsx | 152 +++++++++++++++ .../WorkspaceDuplicateSelectFeaturesPage.tsx | 26 +++ src/types/form/WorkspaceDuplicateForm.tsx | 18 ++ src/types/form/index.ts | 1 + src/types/onyx/DuplicateWorkspace.ts | 22 +++ src/types/onyx/index.ts | 2 + 27 files changed, 622 insertions(+), 29 deletions(-) create mode 100644 src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx create mode 100644 src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx create mode 100644 src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx create mode 100644 src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx create mode 100644 src/types/form/WorkspaceDuplicateForm.tsx create mode 100644 src/types/onyx/DuplicateWorkspace.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index cfb6a38be0a11..3011c7d8f9b1b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -453,6 +453,9 @@ const ONYXKEYS = { NVP_PRIVATE_CANCELLATION_DETAILS: 'nvp_private_cancellationDetails', + /** Stores the information about duplicated workspace */ + DUPLICATE_WORKSPACE: 'duplicateWorkspace', + /** Stores the information about currently edited advanced approval workflow */ APPROVAL_WORKFLOW: 'approvalWorkflow', @@ -660,6 +663,8 @@ const ONYXKEYS = { WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CONFIRMATION_FORM: 'workspaceConfirmationForm', WORKSPACE_CONFIRMATION_FORM_DRAFT: 'workspaceConfirmationFormDraft', + WORKSPACE_DUPLICATE_FORM: 'workspaceDuplicateForm', + WORKSPACE_DUPLICATE_FORM_DRAFT: 'workspaceDuplicateFormDraft', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft', @@ -859,6 +864,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM]: FormTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FORM]: FormTypes.WorkspaceCategoryForm; [ONYXKEYS.FORMS.WORKSPACE_CONFIRMATION_FORM]: FormTypes.WorkspaceConfirmationForm; + [ONYXKEYS.FORMS.WORKSPACE_DUPLICATE_FORM]: FormTypes.WorkspaceDuplicateForm; [ONYXKEYS.FORMS.ONBOARDING_WORKSPACE_DETAILS_FORM]: FormTypes.WorkspaceConfirmationForm; [ONYXKEYS.FORMS.WORKSPACE_TAG_FORM]: FormTypes.WorkspaceTagForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; @@ -1156,6 +1162,7 @@ type OnyxValuesMapping = { [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.ADD_NEW_COMPANY_CARD]: OnyxTypes.AddNewCompanyCardFeed; [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard; + [ONYXKEYS.DUPLICATE_WORKSPACE]: OnyxTypes.DuplicateWorkspace; [ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7fc8e1366a3c5..9c9d09222fb7a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1821,6 +1821,14 @@ const ROUTES = { return getUrlWithBackToParam(`workspaces/${policyID}/per-diem`, backTo); }, }, + WORKSPACE_DUPLICATE: { + route: 'workspace/:policyID/duplicate', + getRoute: (policyID: string) => `workspace/${policyID}/duplicate` as const, + }, + WORKSPACE_DUPLICATE_SELECT_FEATURES: { + route: 'workspace/:policyID/duplicate/select-features', + getRoute: (policyID: string) => `workspace/${policyID}/duplicate/select-features` as const, + }, WORKSPACE_PER_DIEM_IMPORT: { route: 'workspaces/:policyID/per-diem/import', getRoute: (policyID: string) => `workspaces/${policyID}/per-diem/import` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3d5c5bed63cd3..785aa811cd831 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -186,6 +186,7 @@ const SCREENS = { REPORT_DETAILS: 'Report_Details', REPORT_CHANGE_WORKSPACE: 'ReportChangeWorkspace', WORKSPACE_CONFIRMATION: 'Workspace_Confirmation', + WORKSPACE_DUPLICATE: 'Workspace_Duplicate', REPORT_SETTINGS: 'Report_Settings', REPORT_DESCRIPTION: 'Report_Description', PARTICIPANTS: 'Participants', @@ -372,6 +373,7 @@ const SCREENS = { }, WORKSPACE_CONFIRMATION: {ROOT: 'Workspace_Confirmation_Root'}, + WORKSPACE_DUPLICATE: {ROOT: 'Workspace_Duplicate_Root', SELECT_FEATURES: 'Workspace_Duplicate_Select_Features'}, WORKSPACES_LIST: 'Workspaces_List', diff --git a/src/languages/de.ts b/src/languages/de.ts index 1a37f9b6e5d71..59e4b394b6fec 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -296,6 +296,7 @@ const translations = { count: 'Zählen', cancel: 'Abbrechen', dismiss: 'Verwerfen', + proceed: 'Proceed', yes: 'Ja', no: 'No', ok: 'OK', @@ -3400,6 +3401,8 @@ const translations = { memberNotFound: 'Mitglied nicht gefunden. Um ein neues Mitglied zum Arbeitsbereich einzuladen, verwenden Sie bitte die Einladungsschaltfläche oben.', notAuthorized: `Sie haben keinen Zugriff auf diese Seite. Wenn Sie versuchen, diesem Arbeitsbereich beizutreten, bitten Sie einfach den Besitzer des Arbeitsbereichs, Sie als Mitglied hinzuzufügen. Etwas anderes? Kontaktieren Sie ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Zum Arbeitsbereich gehen', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Zu Arbeitsbereichen gehen', clearFilter: 'Filter löschen', workspaceName: 'Arbeitsbereichsname', @@ -4781,6 +4784,13 @@ const translations = { taxCode: 'Steuercode', updateTaxCodeFailureMessage: 'Beim Aktualisieren des Steuercodes ist ein Fehler aufgetreten, bitte versuchen Sie es erneut.', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Erstellen Sie einen Arbeitsbereich', subtitle: diff --git a/src/languages/en.ts b/src/languages/en.ts index 5d7ad2abe8287..56ddbdfc014c4 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -287,6 +287,7 @@ const translations = { count: 'Count', cancel: 'Cancel', dismiss: 'Dismiss', + proceed: 'Proceed', yes: 'Yes', no: 'No', ok: 'OK', @@ -3391,6 +3392,8 @@ const translations = { memberNotFound: 'Member not found. To invite a new member to the workspace, please use the invite button above.', notAuthorized: `You don't have access to this page. If you're trying to join this workspace, just ask the workspace owner to add you as a member. Something else? Reach out to ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Go to workspace', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Go to workspaces', clearFilter: 'Clear filter', workspaceName: 'Workspace name', @@ -4758,6 +4761,14 @@ const translations = { taxCode: 'Tax code', updateTaxCodeFailureMessage: 'An error occurred while updating the tax code, please try again', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Create a workspace', subtitle: 'Create a workspace to track receipts, reimburse expenses, manage travel, send invoices, and more — all at the speed of chat.', diff --git a/src/languages/es.ts b/src/languages/es.ts index c331f1e3f741d..4a3d960ee82d5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -278,6 +278,7 @@ const translations = { count: 'Contar', cancel: 'Cancelar', dismiss: 'Descartar', + proceed: 'Proceed', yes: 'Sí', no: 'No', ok: 'OK', @@ -3382,6 +3383,8 @@ const translations = { memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón invitar que está arriba.', notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a este espacio de trabajo, pide al dueño del espacio de trabajo que te añada como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`, goToWorkspace: 'Ir al espacio de trabajo', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Ir a espacios de trabajo', clearFilter: 'Borrar filtro', workspaceName: 'Nombre del espacio de trabajo', @@ -4772,6 +4775,13 @@ const translations = { taxCode: 'Código de impuesto', updateTaxCodeFailureMessage: 'Se produjo un error al actualizar el código tributario, inténtelo nuevamente', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Crea un espacio de trabajo', subtitle: 'Crea un espacio de trabajo para organizar recibos, reembolsar gastos, gestionar viajes, enviar facturas y mucho más, todo a la velocidad del chat.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c2994e5a9f9b3..f5acf9c60e925 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -296,6 +296,7 @@ const translations = { count: 'Compter', cancel: 'Annuler', dismiss: 'Ignorer', + proceed: 'Proceed', yes: 'Oui', no: 'No', ok: "D'accord", @@ -3405,6 +3406,8 @@ const translations = { memberNotFound: "Membre introuvable. Pour inviter un nouveau membre à l'espace de travail, veuillez utiliser le bouton d'invitation ci-dessus.", notAuthorized: `Vous n'avez pas accès à cette page. Si vous essayez de rejoindre cet espace de travail, demandez simplement au propriétaire de l'espace de travail de vous ajouter en tant que membre. Autre chose ? Contactez ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: "Aller à l'espace de travail", + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Aller aux espaces de travail', clearFilter: 'Effacer le filtre', workspaceName: "Nom de l'espace de travail", @@ -4794,6 +4797,14 @@ const translations = { taxCode: 'Code fiscal', updateTaxCodeFailureMessage: "Une erreur s'est produite lors de la mise à jour du code fiscal, veuillez réessayer.", }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Créer un espace de travail', subtitle: 'Créez un espace de travail pour suivre les reçus, rembourser les dépenses, gérer les voyages, envoyer des factures, et plus encore — le tout à la vitesse du chat.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 397f4833dfe11..795fa2f7d8dbc 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -296,6 +296,7 @@ const translations = { count: 'Contare', cancel: 'Annulla', dismiss: 'Ignora', + proceed: 'Proceed', yes: 'Sì', no: 'No', ok: 'OK', @@ -3411,6 +3412,8 @@ const translations = { memberNotFound: 'Membro non trovato. Per invitare un nuovo membro al workspace, utilizza il pulsante di invito sopra.', notAuthorized: `Non hai accesso a questa pagina. Se stai cercando di unirti a questo spazio di lavoro, chiedi semplicemente al proprietario dello spazio di lavoro di aggiungerti come membro. Qualcos'altro? Contatta ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Vai allo spazio di lavoro', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Vai agli spazi di lavoro', clearFilter: 'Cancella filtro', workspaceName: 'Nome del workspace', @@ -4794,6 +4797,13 @@ const translations = { taxCode: 'Codice fiscale', updateTaxCodeFailureMessage: "Si è verificato un errore durante l'aggiornamento del codice fiscale, riprova.", }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: "Crea un'area di lavoro", subtitle: 'Crea uno spazio di lavoro per tracciare le ricevute, rimborsare le spese, gestire i viaggi, inviare fatture e altro ancora, tutto alla velocità della chat.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index e248d565ecd2a..8a3613808d997 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -296,6 +296,7 @@ const translations = { count: 'カウント', cancel: 'キャンセル', dismiss: '却下する', + proceed: 'Proceed', yes: 'はい', no: 'いいえ', ok: 'OK', @@ -3413,6 +3414,8 @@ const translations = { memberNotFound: 'メンバーが見つかりません。新しいメンバーをワークスペースに招待するには、上の招待ボタンを使用してください。', notAuthorized: `このページにアクセスする権限がありません。このワークスペースに参加しようとしている場合は、ワークスペースのオーナーにメンバーとして追加してもらってください。他に何かお困りですか?${CONST.EMAIL.CONCIERGE}にお問い合わせください。`, goToWorkspace: 'ワークスペースに移動', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'ワークスペースに移動', clearFilter: 'フィルターをクリア', workspaceName: 'ワークスペース名', @@ -4775,6 +4778,13 @@ const translations = { taxCode: '税コード', updateTaxCodeFailureMessage: '税コードの更新中にエラーが発生しました。もう一度お試しください。', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'ワークスペースを作成', subtitle: '領収書を追跡し、経費を払い戻し、旅行を管理し、請求書を送信するためのワークスペースを作成し、チャットの速度でこれらすべてを行いましょう。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 36a6a1eaac9ea..a2c3bb875363f 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -296,6 +296,7 @@ const translations = { count: 'Aantal', cancel: 'Annuleren', dismiss: 'Verwijderen', + proceed: 'Proceed', yes: 'Ja', no: 'No', ok: 'OK', @@ -3418,6 +3419,8 @@ const translations = { memberNotFound: 'Lid niet gevonden. Om een nieuw lid aan de werkruimte toe te voegen, gebruik de uitnodigingsknop hierboven.', notAuthorized: `Je hebt geen toegang tot deze pagina. Als je probeert lid te worden van deze werkruimte, vraag dan de eigenaar van de werkruimte om je als lid toe te voegen. Iets anders? Neem contact op met ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Ga naar werkruimte', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Ga naar werkruimtes', clearFilter: 'Filter wissen', workspaceName: 'Werkruimte naam', @@ -4796,6 +4799,13 @@ const translations = { taxCode: 'Belastingcode', updateTaxCodeFailureMessage: 'Er is een fout opgetreden bij het bijwerken van de belastingcode, probeer het opnieuw.', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Maak een werkruimte aan', subtitle: 'Maak een werkruimte om bonnetjes bij te houden, uitgaven te vergoeden, reizen te beheren, facturen te versturen en meer — allemaal op de snelheid van chat.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d8596ff3e1681..3a5e3bef427ac 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -296,6 +296,7 @@ const translations = { count: 'Liczba', cancel: 'Anuluj', dismiss: 'Odrzuć', + proceed: 'Proceed', yes: 'Tak', no: 'Nie', ok: 'OK', @@ -3411,6 +3412,8 @@ const translations = { memberNotFound: 'Nie znaleziono członka. Aby zaprosić nowego członka do przestrzeni roboczej, użyj przycisku zaproszenia powyżej.', notAuthorized: `Nie masz dostępu do tej strony. Jeśli próbujesz dołączyć do tego miejsca pracy, poproś właściciela miejsca pracy o dodanie Cię jako członka. Coś innego? Skontaktuj się z ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Przejdź do przestrzeni roboczej', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Przejdź do przestrzeni roboczych', clearFilter: 'Wyczyść filtr', workspaceName: 'Nazwa przestrzeni roboczej', @@ -4785,6 +4788,13 @@ const translations = { taxCode: 'Kod podatkowy', updateTaxCodeFailureMessage: 'Wystąpił błąd podczas aktualizacji kodu podatkowego, spróbuj ponownie.', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Utwórz przestrzeń roboczą', subtitle: 'Utwórz przestrzeń roboczą do śledzenia paragonów, zwracania wydatków, zarządzania podróżami, wysyłania faktur i nie tylko — wszystko z prędkością czatu.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 18caa03c74b75..0ee1f40046644 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -296,6 +296,7 @@ const translations = { count: 'Contagem', cancel: 'Cancelar', dismiss: 'Dispensar', + proceed: 'Proceed', yes: 'Sim', no: 'Não', ok: 'OK', @@ -3415,6 +3416,8 @@ const translations = { memberNotFound: 'Membro não encontrado. Para convidar um novo membro para o espaço de trabalho, por favor, use o botão de convite acima.', notAuthorized: `Você não tem acesso a esta página. Se você está tentando entrar neste espaço de trabalho, basta pedir ao proprietário do espaço de trabalho para adicioná-lo como membro. Algo mais? Entre em contato com ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Ir para o espaço de trabalho', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: 'Ir para espaços de trabalho', clearFilter: 'Limpar filtro', workspaceName: 'Nome do espaço de trabalho', @@ -4790,6 +4793,13 @@ const translations = { taxCode: 'Código fiscal', updateTaxCodeFailureMessage: 'Ocorreu um erro ao atualizar o código de imposto, por favor, tente novamente.', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: 'Criar um espaço de trabalho', subtitle: 'Crie um espaço de trabalho para rastrear recibos, reembolsar despesas, gerenciar viagens, enviar faturas e muito mais — tudo na velocidade do chat.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1d3efbae9d03d..e7c2f6e8b88e0 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -296,6 +296,7 @@ const translations = { count: '计数', cancel: '取消', dismiss: '忽略', + proceed: 'Proceed', yes: '是的', no: '不', ok: '好的', @@ -3375,6 +3376,8 @@ const translations = { memberNotFound: '未找到成员。要邀请新成员加入工作区,请使用上面的邀请按钮。', notAuthorized: `您无权访问此页面。如果您正在尝试加入此工作区,请请求工作区所有者将您添加为成员。还有其他问题?请联系${CONST.EMAIL.CONCIERGE}。`, goToWorkspace: '前往工作区', + duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspacePrefix: 'Duplicate', goToWorkspaces: '前往工作区', clearFilter: '清除筛选器', workspaceName: '工作区名称', @@ -4714,6 +4717,13 @@ const translations = { taxCode: '税码', updateTaxCodeFailureMessage: '更新税码时发生错误,请重试', }, + duplicateWorkspace: { + title: 'Name your new workspace', + selectFeatures: 'Select features to copy', + whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + }, emptyWorkspace: { title: '创建一个工作区', subtitle: '创建一个工作区来跟踪收据、报销费用、管理差旅、发送发票等——一切都在聊天的速度下完成。', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 71502f0415b7e..c65ed5b5473a6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -38,7 +38,7 @@ import type { TransactionDuplicateNavigatorParamList, TravelNavigatorParamList, WalletStatementNavigatorParamList, - WorkspaceConfirmationNavigatorParamList, + WorkspaceConfirmationNavigatorParamList, WorkspaceDuplicateNavigatorParamList, } from '@navigation/types'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; @@ -175,6 +175,11 @@ const WorkspaceConfirmationModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceConfirmationPage').default, }); +const WorkspaceDuplicateModalStackNavigator = createModalStackNavigator({ + [SCREENS.WORKSPACE_DUPLICATE.ROOT]: () => require('../../../../pages/workspace/duplicate/WorkspaceDuplicatePage').default, + [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES]: () => require('../../../../pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage').default, +}); + const TaskModalStackNavigator = createModalStackNavigator({ [SCREENS.TASK.TITLE]: () => require('../../../../pages/tasks/TaskTitlePage').default, [SCREENS.TASK.ASSIGNEE]: () => require('../../../../pages/tasks/TaskAssigneeSelectorModal').default, @@ -828,6 +833,7 @@ export { MissingPersonalDetailsModalStackNavigator, DebugModalStackNavigator, WorkspaceConfirmationModalStackNavigator, + WorkspaceDuplicateModalStackNavigator, ConsoleModalStackNavigator, AddUnreportedExpenseModalStackNavigator, ScheduleCallModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 03752df588dc8..0542ba0bdd677 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -144,6 +144,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION} component={ModalStackNavigators.WorkspaceConfirmationModalStackNavigator} /> + ['config'] = { [SCREENS.WORKSPACE_CONFIRMATION.ROOT]: ROUTES.WORKSPACE_CONFIRMATION.route, }, }, + [SCREENS.RIGHT_MODAL.WORKSPACE_DUPLICATE]: { + screens: { + [SCREENS.WORKSPACE_DUPLICATE.ROOT]: ROUTES.WORKSPACE_DUPLICATE.route, + [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES]: ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.route, + }, + }, [SCREENS.RIGHT_MODAL.NEW_TASK]: { screens: { [SCREENS.NEW_TASK.ROOT]: ROUTES.NEW_TASK.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0a8db96592b03..c064d08667de3 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1522,6 +1522,15 @@ type WorkspaceConfirmationNavigatorParamList = { }; }; +type WorkspaceDuplicateNavigatorParamList = { + [SCREENS.WORKSPACE_DUPLICATE.ROOT]: { + policyID: string; + }; + [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES]: { + policyID: string; + }; +}; + type NewTaskNavigatorParamList = { [SCREENS.NEW_TASK.ROOT]: { backTo?: Routes; @@ -1697,6 +1706,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.WORKSPACE_CONFIRMATION]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.WORKSPACE_DUPLICATE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams; @@ -2277,6 +2287,7 @@ export type { WorkspaceSplitNavigatorParamList, MigratedUserModalNavigatorParamList, WorkspaceConfirmationNavigatorParamList, + WorkspaceDuplicateNavigatorParamList, TwoFactorAuthNavigatorParamList, ConsoleNavigatorParamList, ScheduleCallParamList, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 021e3c4d284f9..1fee530c1aaa5 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -100,11 +100,13 @@ import type { Transaction, TransactionViolations, } from '@src/types/onyx'; +import type {AddNewCardFeedData} from '@src/types/onyx/CardFeeds'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, ProhibitedExpenses, Rate, TaxRate} from '@src/types/onyx/Policy'; import type {CustomFieldType} from '@src/types/onyx/PolicyEmployee'; import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import DuplicateWorkspace from '../../../types/onyx/DuplicateWorkspace'; import {buildOptimisticMccGroup, buildOptimisticPolicyCategories} from './Category'; type ReportCreationData = Record< @@ -1660,6 +1662,14 @@ function removeWorkspace(policyID: string) { Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, null); } +function setDuplicateWorkspaceData(data: Partial) { + Onyx.merge(ONYXKEYS.DUPLICATE_WORKSPACE, {...data}); +} + +function clearDuplicateWorkspace() { + Onyx.set(ONYXKEYS.DUPLICATE_WORKSPACE, {}); +} + /** * Generate a policy name based on an email and policy list. * @param [email] the email to base the workspace name on. If not passed, will use the logged-in user's email instead @@ -5487,6 +5497,8 @@ export { setPolicyMaxExpenseAge, updateCustomRules, setPolicyProhibitedExpense, + setDuplicateWorkspaceData, + clearDuplicateWorkspace, setPolicyBillableMode, disableWorkspaceBillableExpenses, setWorkspaceEReceiptsEnabled, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d592fdb1e88ad..bd6129a6ad526 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -185,35 +185,42 @@ function WorkspacesListPage() { ]; if (isOwner) { - threeDotsMenuItems.push({ - icon: Expensicons.Trashcan, - text: translate('workspace.common.delete'), - shouldShowLoadingSpinnerIcon: loadingSpinnerIconIndex === index, - onSelected: () => { - if (loadingSpinnerIconIndex !== null) { - return; - } - - if (isSupportalAction) { - setIsSupportalActionRestrictedModalOpen(true); - return; - } - - setPolicyIDToDelete(item.policyID); - setPolicyNameToDelete(item.title); - - if (shouldCalculateBillNewDot) { - setIsDeletingPaidWorkspace(true); - calculateBillNewDot(); - setLoadingSpinnerIconIndex(index); - return; - } - - setIsDeleteModalOpen(true); + threeDotsMenuItems.push( + { + icon: Expensicons.Trashcan, + text: translate('workspace.common.delete'), + shouldShowLoadingSpinnerIcon: loadingSpinnerIconIndex === index, + onSelected: () => { + if (loadingSpinnerIconIndex !== null) { + return; + } + + if (isSupportalAction) { + setIsSupportalActionRestrictedModalOpen(true); + return; + } + + setPolicyIDToDelete(item.policyID); + setPolicyNameToDelete(item.title); + + if (shouldCalculateBillNewDot) { + setIsDeletingPaidWorkspace(true); + calculateBillNewDot(); + setLoadingSpinnerIconIndex(index); + return; + } + + setIsDeleteModalOpen(true); + }, + shouldKeepModalOpen: shouldCalculateBillNewDot, + shouldCallAfterModalHide: !shouldCalculateBillNewDot, }, - shouldKeepModalOpen: shouldCalculateBillNewDot, - shouldCallAfterModalHide: !shouldCalculateBillNewDot, - }); + { + icon: Expensicons.Copy, + text: translate('workspace.common.duplicateWorkspace'), + onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE.getRoute(item.policyID)) : undefined), + }, + ); } if (!(isAdmin || isOwner)) { diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx new file mode 100644 index 0000000000000..d246facf88fb1 --- /dev/null +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -0,0 +1,174 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import Avatar from '@components/Avatar'; +import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {generatePolicyID, setDuplicateWorkspaceData} from '@libs/actions/Policy/Policy'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import {addErrorMessage} from '@libs/ErrorUtils'; +import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; +import {isRequiredFulfilled} from '@libs/ValidationUtils'; +import Navigation from '@navigation/Navigation'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import INPUT_IDS from '@src/types/form/WorkspaceDuplicateForm'; + +function getFirstAlphaNumericCharacter(str = '') { + return str + .normalize('NFD') + .replace(/[^0-9a-z]/gi, '') + .toUpperCase()[0]; +} + +type WorkspaceDuplicateFormProps = { + policyID?: string; +}; + +function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const policy = usePolicy(policyID); + const defaultWorkspaceName = `${policy?.name} (${translate('workspace.common.duplicateWorkspacePrefix')})`; + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; + const name = values.name.trim(); + + if (!isRequiredFulfilled(name)) { + errors.name = translate('workspace.editor.nameIsRequiredError'); + } else if ([...name].length > CONST.TITLE_CHARACTER_LIMIT) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 + // code units. + addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } + + return errors; + }, + [translate], + ); + + const onSubmit = useCallback(({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { + const newPolicyID = generatePolicyID(); + setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); + if (policyID) { + Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); + } + }, [policyID]); + + const [workspaceNameFirstCharacter, setWorkspaceNameFirstCharacter] = useState(defaultWorkspaceName ?? ''); + + const [workspaceAvatar, setWorkspaceAvatar] = useState<{avatarUri: string | null; avatarFileName?: string | null; avatarFileType?: string | null}>({ + avatarUri: null, + avatarFileName: null, + avatarFileType: null, + }); + const [avatarFile, setAvatarFile] = useState(); + + const stashedLocalAvatarImage = workspaceAvatar?.avatarUri ?? undefined; + + const DefaultAvatar = useCallback( + () => ( + + ), + [workspaceAvatar?.avatarUri, workspaceNameFirstCharacter, styles.alignSelfCenter, styles.avatarXLarge, policyID], + ); + + return ( + <> + + + + {translate('workspace.duplicateWorkspace.title')} + + { + setAvatarFile(image); + setWorkspaceAvatar({avatarUri: image.uri ?? '', avatarFileName: image.name ?? '', avatarFileType: image.type}); + }} + onImageRemoved={() => { + setAvatarFile(undefined); + setWorkspaceAvatar({avatarUri: null, avatarFileName: null, avatarFileType: null}); + }} + size={CONST.AVATAR_SIZE.X_LARGE} + avatarStyle={[styles.avatarXLarge, styles.alignSelfCenter]} + shouldDisableViewPhoto + editIcon={Expensicons.Camera} + editIconStyle={styles.smallEditIconAccount} + type={CONST.ICON_TYPE_WORKSPACE} + style={[styles.w100, styles.alignItemsCenter, styles.mv4, styles.mb6, styles.alignSelfCenter, styles.ph5]} + DefaultAvatar={DefaultAvatar} + editorMaskImage={Expensicons.ImageCropSquareMask} + /> + + onSubmit({ + name: val[INPUT_IDS.NAME], + avatarFile, + }) + } + enabledWhenOffline + addBottomSafeAreaPadding + > + + { + if (getFirstAlphaNumericCharacter(str) === getFirstAlphaNumericCharacter(workspaceNameFirstCharacter)) { + return; + } + setWorkspaceNameFirstCharacter(str); + }} + ref={inputCallbackRef} + /> + + + + + ); +} + +WorkspaceDuplicateForm.displayName = 'WorkspaceDuplicateForm'; + +export default WorkspaceDuplicateForm; diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx new file mode 100644 index 0000000000000..cbd3d8c1a9e66 --- /dev/null +++ b/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx @@ -0,0 +1,33 @@ +import {useRoute} from '@react-navigation/native'; +import React, {useEffect} from 'react'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; +import type {WorkspaceDuplicateNavigatorParamList} from '@navigation/types'; +import {clearDuplicateWorkspace} from '@userActions/Policy/Policy'; +import type SCREENS from '@src/SCREENS'; +import WorkspaceDuplicateForm from './WorkspaceDuplicateForm'; + +function WorkspaceDuplicatePage() { + const route = useRoute>(); + const policyID = route?.params?.policyID; + + useEffect(() => { + return () => { + clearDuplicateWorkspace(); + }; + }, []); + + return ( + + + + ); +} + +WorkspaceDuplicatePage.displayName = 'WorkspaceDuplicatePage'; + +export default WorkspaceDuplicatePage; diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx new file mode 100644 index 0000000000000..122a84546b8c3 --- /dev/null +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -0,0 +1,152 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScrollView from '@components/ScrollView'; +import SelectionList from '@components/SelectionList'; +import MultiSelectListItem from '@components/SelectionList/MultiSelectListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; +import {getStatusOptions} from '@libs/SearchUIUtils'; +import {openWorkspaceMembersPage} from '@userActions/Policy/Member'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type WorkspaceDuplicateFormProps = { + policyID?: string; +}; + +function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateFormProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const policy = usePolicy(policyID); + const [duplicateWorkspace] = useOnyx(ONYXKEYS.DUPLICATE_WORKSPACE, {canBeMissing: false}); + const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false); + const allIds = getMemberAccountIDsForWorkspace(policy?.employeeList); + const totalMembers = allIds.length; + const [selectedItems, setSelectedItems] = useState([]); + + const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); + const formattedAddress = + !isEmptyObject(policy) && !isEmptyObject(policy.address) + ? `${street1?.trim()}, ${street2 ? `${street2.trim()}, ` : ''}${policy.address.city}, ${policy.address.state} ${policy.address.zipCode ?? ''}` + : ''; + + const items = useMemo(() => { + const result = [ + { + translation: translate('workspace.common.profile'), + value: 'overview', + alternateText: `${policy?.name}, ${policy?.outputCurrency} ${translate('common.currency')}, ${formattedAddress}`, + }, + { + translation: translate('workspace.common.members'), + value: 'members', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.members'), + value: 'members', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.members'), + value: 'members', + alternateText: 'eqwewq', + }, + ]; + + return result; + }, [formattedAddress, policy?.name, policy?.outputCurrency, translate]); + + const listData: ListItem[] = useMemo(() => { + return items.map((option) => ({ + text: option.translation, + keyForList: option.value, + isSelected: selectedItems.includes(option.value), + alternateText: option.alternateText, + })); + }, [items, selectedItems]); + + const getWorkspaceMembers = useCallback(() => { + if (!policyID) { + return; + } + openWorkspaceMembersPage(policyID, Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList))); + }, [policyID, policy?.employeeList]); + + const confirmDuplicateAndHideModal = useCallback(() => { + setIsDuplicateModalOpen(false); + }, []); + + const updateSelectedItems = useCallback( + (listItem: ListItem) => { + if (listItem.isSelected) { + setSelectedItems(selectedItems.filter((i) => i !== listItem.keyForList)); + return; + } + + const newItem = items.find((i) => i.value === listItem.keyForList)?.value; + + if (newItem) { + setSelectedItems([...selectedItems, newItem]); + } + }, + [items, selectedItems], + ); + + useEffect(() => { + getWorkspaceMembers(); + }, [getWorkspaceMembers]); + + return ( + <> + + + + {translate('workspace.duplicateWorkspace.selectFeatures')} + {translate('workspace.duplicateWorkspace.whichFeatures')} + + + + + + setIsDuplicateModalOpen(false)} + prompt={ + + + {translate('workspace.duplicateWorkspace.confirmTitle', {newWorkspaceName: duplicateWorkspace?.name, oldWorkspaceName: policy?.name, totalMembers})} + + {translate('workspace.duplicateWorkspace.confirmDuplicate')} + + } + confirmText={translate('common.proceed')} + cancelText={translate('common.cancel')} + success + /> + + ); +} + +WorkspaceDuplicateSelectFeaturesForm.displayName = 'WorkspaceDuplicateSelectFeaturesForm'; + +export default WorkspaceDuplicateSelectFeaturesForm; diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx new file mode 100644 index 0000000000000..ef3b24f52dcc4 --- /dev/null +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx @@ -0,0 +1,26 @@ +import {useRoute} from '@react-navigation/native'; +import React from 'react'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; +import type {WorkspaceDuplicateNavigatorParamList} from '@navigation/types'; +import type SCREENS from '@src/SCREENS'; +import WorkspaceDuplicateSelectFeaturesForm from './WorkspaceDuplicateSelectFeaturesForm'; + +function WorkspaceDuplicateSelectFeaturesPage() { + const route = useRoute>(); + const policyID = route?.params?.policyID; + + return ( + + + + ); +} + +WorkspaceDuplicateSelectFeaturesPage.displayName = 'WorkspaceDuplicateSelectFeaturesPage'; + +export default WorkspaceDuplicateSelectFeaturesPage; diff --git a/src/types/form/WorkspaceDuplicateForm.tsx b/src/types/form/WorkspaceDuplicateForm.tsx new file mode 100644 index 0000000000000..348e2e3500e84 --- /dev/null +++ b/src/types/form/WorkspaceDuplicateForm.tsx @@ -0,0 +1,18 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + NAME: 'name', +} as const; + +type InputID = ValueOf; + +type WorkspaceDuplicateForm = Form< + InputID, + { + [INPUT_IDS.NAME]: string; + } +>; + +export type {WorkspaceDuplicateForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index ff6cc48835371..14c98ce034c1a 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -34,6 +34,7 @@ export type {ReportVirtualCardFraudForm} from './ReportVirtualCardFraudForm'; export type {DebugReportForm} from './DebugReportForm'; export type {DebugReportActionForm} from './DebugReportActionForm'; export type {DebugTransactionForm} from './DebugTransactionForm'; +export type {WorkspaceDuplicateForm} from './WorkspaceDuplicateForm'; export type {DebugTransactionViolationForm} from './DebugTransactionViolationForm'; export type {RequestPhysicalCardForm} from './RequestPhysicalCardForm'; export type {RoomNameForm} from './RoomNameForm'; diff --git a/src/types/onyx/DuplicateWorkspace.ts b/src/types/onyx/DuplicateWorkspace.ts new file mode 100644 index 0000000000000..2ee68beb6a208 --- /dev/null +++ b/src/types/onyx/DuplicateWorkspace.ts @@ -0,0 +1,22 @@ +import type {CustomRNImageManipulatorResult} from "@libs/cropOrRotateImage/types"; +import type * as OnyxCommon from './OnyxCommon'; + +/** Model of plaid data */ +type DuplicateWorkspace = { + /** New policy ID */ + policyID?: string; + + /** New workspace name */ + name?: string; + + /** Workspace avatar */ + file?: File | CustomRNImageManipulatorResult + + /** Whether the data is being fetched from server */ + isLoading?: boolean; + + /** Error messages to show in UI */ + errors?: OnyxCommon.Errors; +}; + +export default DuplicateWorkspace; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 24a921c1c9e5c..d23934d4dcac9 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -58,6 +58,7 @@ import type PersonalBankAccount from './PersonalBankAccount'; import type {PersonalDetailsList, PersonalDetailsMetadata} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; +import type DuplicateWorkspace from './DuplicateWorkspace'; import type Policy from './Policy'; import type {PolicyConnectionName, PolicyConnectionSyncProgress, PolicyReportField, TaxRate, TaxRates, TaxRatesWithDefault} from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; @@ -139,6 +140,7 @@ export type { CurrencyList, CustomStatusDraft, DismissedReferralBanners, + DuplicateWorkspace, Download, WorkspaceCardsList, ExpensifyCardSettings, From 77040574553ef862281fed595fbd0623a347ad85 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 22 Jul 2025 15:52:50 +0200 Subject: [PATCH 02/29] feat: add missing translations and missing fields --- src/languages/de.ts | 9 ++-- src/languages/en.ts | 2 + src/languages/es.ts | 3 ++ src/languages/fr.ts | 8 +-- src/languages/it.ts | 5 +- src/languages/ja.ts | 3 ++ src/languages/nl.ts | 3 ++ src/languages/pl.ts | 5 +- src/languages/pt-BR.ts | 3 ++ src/languages/zh-hans.ts | 3 ++ .../WorkspaceDuplicateSelectFeaturesForm.tsx | 49 ++++++++++++++++++- 11 files changed, 83 insertions(+), 10 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index a611b4785aefb..ea8b4eedf253c 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -297,7 +297,7 @@ const translations = { count: 'Zählen', cancel: 'Abbrechen', dismiss: 'Verwerfen', - proceed: 'Proceed', + proceed: 'Fortfahren', yes: 'Ja', no: 'No', ok: 'OK', @@ -3396,12 +3396,14 @@ const translations = { customField1: 'Benutzerdefiniertes Feld 1', customField2: 'Benutzerdefiniertes Feld 2', customFieldHint: 'Fügen Sie benutzerdefinierten Code hinzu, der für alle Ausgaben dieses Mitglieds gilt.', + reports: 'Berichte', reportFields: 'Berichtsfelder', reportTitle: 'Berichtstitel', reportField: 'Berichtsfeld', taxes: 'Steuern', bills: 'Rechnungen', invoices: 'Rechnungen', + perDiem: 'Per diem', travel: 'Reisen', members: 'Mitglieder', accounting: 'Buchhaltung', @@ -3427,8 +3429,8 @@ const translations = { memberNotFound: 'Mitglied nicht gefunden. Um ein neues Mitglied zum Arbeitsbereich einzuladen, verwenden Sie bitte die Einladungsschaltfläche oben.', notAuthorized: `Sie haben keinen Zugriff auf diese Seite. Wenn Sie versuchen, diesem Arbeitsbereich beizutreten, bitten Sie einfach den Besitzer des Arbeitsbereichs, Sie als Mitglied hinzuzufügen. Etwas anderes? Kontaktieren Sie ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Zum Arbeitsbereich gehen', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'Arbeitsbereich duplizieren', + duplicateWorkspacePrefix: 'Duplizieren', goToWorkspaces: 'Zu Arbeitsbereichen gehen', clearFilter: 'Filter löschen', workspaceName: 'Arbeitsbereichsname', @@ -4826,6 +4828,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index cf799e9a21692..bc01d97a75141 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3388,12 +3388,14 @@ const translations = { customField1: 'Custom field 1', customField2: 'Custom field 2', customFieldHint: 'Add custom coding that applies to all spend from this member.', + reports: 'Reports', reportFields: 'Report fields', reportTitle: 'Report title', reportField: 'Report field', taxes: 'Taxes', bills: 'Bills', invoices: 'Invoices', + perDiem: 'Per diem', travel: 'Travel', members: 'Members', accounting: 'Accounting', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5d3150e14da87..89339af8e620f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3380,11 +3380,13 @@ const translations = { customField1: 'Campo personalizado 1', customField2: 'Campo personalizado 2', customFieldHint: 'Añade una codificación personalizada que se aplique a todos los gastos de este miembro.', + reports: 'Informes', reportFields: 'Campos de informe', reportTitle: 'El título del informe.', taxes: 'Impuestos', bills: 'Pagar facturas', invoices: 'Facturas', + perDiem: 'Per diem', travel: 'Viajes', members: 'Miembros', accounting: 'Contabilidad', @@ -4818,6 +4820,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b45f981b0eb1b..95338cae96127 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -297,7 +297,7 @@ const translations = { count: 'Compter', cancel: 'Annuler', dismiss: 'Ignorer', - proceed: 'Proceed', + proceed: 'Procéder', yes: 'Oui', no: 'No', ok: "D'accord", @@ -3402,12 +3402,14 @@ const translations = { customField1: 'Champ personnalisé 1', customField2: 'Champ personnalisé 2', customFieldHint: "Ajoutez un codage personnalisé qui s'applique à toutes les dépenses de ce membre.", + reports: 'Rapports', reportFields: 'Champs de rapport', reportTitle: 'Titre du rapport', reportField: 'Champ de rapport', taxes: 'Taxes', bills: 'Bills', invoices: 'Factures', + perDiem: 'Per diem', travel: 'Voyage', members: 'Membres', accounting: 'Comptabilité', @@ -3433,8 +3435,8 @@ const translations = { memberNotFound: "Membre introuvable. Pour inviter un nouveau membre à l'espace de travail, veuillez utiliser le bouton d'invitation ci-dessus.", notAuthorized: `Vous n'avez pas accès à cette page. Si vous essayez de rejoindre cet espace de travail, demandez simplement au propriétaire de l'espace de travail de vous ajouter en tant que membre. Autre chose ? Contactez ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: "Aller à l'espace de travail", - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'Dupliquer l’espace de travail', + duplicateWorkspacePrefix: 'Dupliquer', goToWorkspaces: 'Aller aux espaces de travail', clearFilter: 'Effacer le filtre', workspaceName: "Nom de l'espace de travail", diff --git a/src/languages/it.ts b/src/languages/it.ts index 71636b4c963ce..3393ea99d2801 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3408,12 +3408,14 @@ const translations = { customField1: 'Campo personalizzato 1', customField2: 'Campo personalizzato 2', customFieldHint: 'Aggiungi una codifica personalizzata che si applica a tutte le spese di questo membro.', + reports: 'Rapporti', reportFields: 'Campi del rapporto', reportTitle: 'Titolo del rapporto', - reportField: 'Campo del report', + reportField: 'Campo del rapporto', taxes: 'Tasse', bills: 'Fatture', invoices: 'Fatture', + perDiem: 'Per diem', travel: 'Viaggio', members: 'Membri', accounting: 'Contabilità', @@ -4840,6 +4842,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c8303efd602fc..3f406ed2eadb1 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3409,12 +3409,14 @@ const translations = { customField1: 'カスタムフィールド1', customField2: 'カスタムフィールド2', customFieldHint: 'このメンバーのすべての支出に適用されるカスタムコーディングを追加します。', + reports: 'レポート', reportFields: 'レポートフィールド', reportTitle: 'レポートタイトル', reportField: 'レポートフィールド', taxes: '税金', bills: '請求書', invoices: '請求書', + perDiem: 'Per diem', travel: '旅行', members: 'メンバー', accounting: '会計', @@ -4820,6 +4822,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index d875fdbe1dc98..47d66ae1d563d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3415,12 +3415,14 @@ const translations = { customField1: 'Aangepast veld 1', customField2: 'Aangepast veld 2', customFieldHint: 'Voeg aangepaste codering toe die van toepassing is op alle uitgaven van dit lid.', + reports: 'Rapporten', reportFields: 'Rapportvelden', reportTitle: 'Rapporttitel', reportField: 'Rapportveld', taxes: 'Belastingen', bills: 'Rekeningen', invoices: 'Facturen', + perDiem: 'Per diem', travel: 'Reis', members: 'Leden', accounting: 'Boekhouding', @@ -4842,6 +4844,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5cb913cf1d872..805b6ffc6275d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3408,13 +3408,15 @@ const translations = { customField1: 'Pole niestandardowe 1', customField2: 'Pole niestandardowe 2', customFieldHint: 'Dodaj niestandardowe kodowanie, które dotyczy wszystkich wydatków tego członka.', + reports: 'Raporty', reportFields: 'Pola raportu', reportTitle: 'Tytuł raportu', reportField: 'Pole raportu', taxes: 'Podatki', bills: 'Rachunki', invoices: 'Faktury', - travel: 'Podróżować', + perDiem: 'Per diem', + travel: 'Podróże', members: 'Członkowie', accounting: 'Księgowość', rules: 'Zasady', @@ -4831,6 +4833,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 4cce0032d7cbe..ee0ca6f66d9b4 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3412,12 +3412,14 @@ const translations = { customField1: 'Campo personalizado 1', customField2: 'Campo personalizado 2', customFieldHint: 'Adicione uma codificação personalizada que se aplique a todos os gastos deste membro.', + reports: 'Relatórios', reportFields: 'Campos do relatório', reportTitle: 'Título do relatório', reportField: 'Campo de relatório', taxes: 'Impostos', bills: 'Faturas', invoices: 'Faturas', + perDiem: 'Per diem', travel: 'Viagem', members: 'Membros', accounting: 'Contabilidade', @@ -4836,6 +4838,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index f45fb3533a04c..f2947d3500c15 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3370,12 +3370,14 @@ const translations = { customField1: '自定义字段 1', customField2: '自定义字段2', customFieldHint: '添加适用于该成员所有支出的自定义编码。', + reports: '报告', reportFields: '报告字段', reportTitle: '报告标题', reportField: '报告字段', taxes: '税款', bills: '账单', invoices: '发票', + perDiem: 'Per diem', travel: '旅行', members: '成员', accounting: '会计', @@ -4758,6 +4760,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', + confirmDuplicate: 'Are you sure you want to proceed?', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 122a84546b8c3..1c9288b2d102e 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -56,8 +56,53 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm alternateText: 'eqwewq', }, { - translation: translate('workspace.common.members'), - value: 'members', + translation: translate('workspace.common.accounting'), + value: 'accounting', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.tags'), + value: 'tags', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.taxes'), + value: 'taxes', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.workflows'), + value: 'workflows', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.rules'), + value: 'rules', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.distanceRates'), + value: 'distanceRates', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.expensifyCard'), + value: 'expensifyCard', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.companyCards'), + value: 'companyCards', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.perDiem'), + value: 'perDiem', + alternateText: 'eqwewq', + }, + { + translation: translate('workspace.common.invoices'), + value: 'invoices', alternateText: 'eqwewq', }, ]; From 4f3ef0f3d6adbc6c8895174a5b395ddd5789dea6 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 23 Jul 2025 17:14:54 +0200 Subject: [PATCH 03/29] feat: add select all button, add old name validation --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + .../duplicate/WorkspaceDuplicateForm.tsx | 5 +- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 71 +++++++++++++------ 12 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index aab04372e877e..79330682a646e 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3417,6 +3417,7 @@ const translations = { testTransactions: 'Transaktionen testen', issueAndManageCards: 'Karten ausstellen und verwalten', reconcileCards: 'Karten abstimmen', + selectAll: 'Alle auswählen', selected: () => ({ one: '1 ausgewählt', other: (count: number) => `${count} ausgewählt`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 63cc1ce080870..e46732b397718 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3409,6 +3409,7 @@ const translations = { testTransactions: 'Test transactions', issueAndManageCards: 'Issue and manage cards', reconcileCards: 'Reconcile cards', + selectAll: 'Select all', selected: () => ({ one: '1 selected', other: (count: number) => `${count} selected`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 8adb1f1be0e19..b2800a800e6b3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3400,6 +3400,7 @@ const translations = { testTransactions: 'Transacciones de prueba', issueAndManageCards: 'Emitir y gestionar tarjetas', reconcileCards: 'Reconciliar tarjetas', + selectAll: 'Seleccionar todo', selected: () => ({ one: '1 seleccionado', other: (count: number) => `${count} seleccionados`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index dc02551a8dbfb..b232b82ae3b1f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3423,6 +3423,7 @@ const translations = { testTransactions: 'Tester les transactions', issueAndManageCards: 'Émettre et gérer des cartes', reconcileCards: 'Rapprocher les cartes', + selectAll: 'Sélectionner tout', selected: () => ({ one: '1 sélectionné', other: (count: number) => `${count} sélectionné(s)`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 99f6c948549bf..4ab0dbe0a0d3b 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3429,6 +3429,7 @@ const translations = { testTransactions: 'Transazioni di prova', issueAndManageCards: 'Emetti e gestisci carte', reconcileCards: 'Riconcilia carte', + selectAll: 'Seleziona tutto', selected: () => ({ one: '1 selezionato', other: (count: number) => `${count} selezionati`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c9cda0cdac2ce..d410fa5e6ecdd 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3430,6 +3430,7 @@ const translations = { testTransactions: 'トランザクションをテストする', issueAndManageCards: 'カードの発行と管理', reconcileCards: 'カードを照合する', + selectAll: 'すべて選択', selected: () => ({ one: '1 件選択済み', other: (count: number) => `${count} 件選択済み`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 1e03d3321f2f9..6a1fb81e7ad5c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3436,6 +3436,7 @@ const translations = { testTransactions: 'Testtransacties', issueAndManageCards: 'Kaarten uitgeven en beheren', reconcileCards: 'Reconcileer kaarten', + selectAll: 'Alles selecteren', selected: () => ({ one: '1 geselecteerd', other: (count: number) => `${count} geselecteerd`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 53a18ad458066..3d7445e3c443a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3429,6 +3429,7 @@ const translations = { testTransactions: 'Przetestuj transakcje', issueAndManageCards: 'Wydawaj i zarządzaj kartami', reconcileCards: 'Uzgodnij karty', + selectAll: 'Wybierz wszystkie', selected: () => ({ one: '1 wybrano', other: (count: number) => `${count} wybrano`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index eaec5250e5c23..742b287141d2e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3433,6 +3433,7 @@ const translations = { testTransactions: 'Testar transações', issueAndManageCards: 'Emitir e gerenciar cartões', reconcileCards: 'Conciliar cartões', + selectAll: 'Selecionar todos', selected: () => ({ one: '1 selecionado', other: (count: number) => `${count} selecionado(s)`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2c03d440a1bf5..f941d38e003eb 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3391,6 +3391,7 @@ const translations = { testTransactions: '测试交易', issueAndManageCards: '发行和管理卡片', reconcileCards: '对账卡片', + selectAll: '全选', selected: () => ({ one: '1 已选择', other: (count: number) => `已选择${count}个`, diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx index d246facf88fb1..0e2443126af7c 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -54,11 +54,14 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 // code units. addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); + } else if (name === policy?.name) { + // TODO: add error message translation + addErrorMessage(errors, 'name', 'the new name cannot be the same as the old policy name'); } return errors; }, - [translate], + [policy?.name, translate], ); const onSubmit = useCallback(({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 1c9288b2d102e..bd2bbe510c495 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -29,7 +29,12 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const [duplicateWorkspace] = useOnyx(ONYXKEYS.DUPLICATE_WORKSPACE, {canBeMissing: false}); const [isDuplicateModalOpen, setIsDuplicateModalOpen] = useState(false); const allIds = getMemberAccountIDsForWorkspace(policy?.employeeList); - const totalMembers = allIds.length; + const totalMembers = Object.keys(allIds).length; + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: false}); + const totalTags = Object.keys(policyTags ?? {}).length ?? 0; + const taxesLength = Object.keys(policy?.taxRates?.taxes ?? {}).length ?? 0; + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); + const categoriesCount = Object.keys(policyCategories ?? {}).length; const [selectedItems, setSelectedItems] = useState([]); const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); @@ -40,6 +45,10 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const items = useMemo(() => { const result = [ + { + translation: translate('workspace.common.selectAll'), + value: 'selectAll', + }, { translation: translate('workspace.common.profile'), value: 'overview', @@ -48,11 +57,11 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm { translation: translate('workspace.common.members'), value: 'members', - alternateText: 'eqwewq', + alternateText: totalMembers ? `${totalMembers} ${translate('workspace.common.members')}` : undefined, }, { - translation: translate('workspace.common.members'), - value: 'members', + translation: translate('workspace.common.reports'), + value: 'reports', alternateText: 'eqwewq', }, { @@ -63,12 +72,17 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm { translation: translate('workspace.common.tags'), value: 'tags', - alternateText: 'eqwewq', + alternateText: totalTags ? `${totalTags} ${translate('workspace.common.tags')}` : undefined, + }, + { + translation: translate('workspace.common.categories'), + value: 'categories', + alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.common.categories')}` : undefined, }, { translation: translate('workspace.common.taxes'), value: 'taxes', - alternateText: 'eqwewq', + alternateText: taxesLength ? `${taxesLength} ${translate('workspace.common.taxes')}` : undefined, }, { translation: translate('workspace.common.workflows'), @@ -85,16 +99,6 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm value: 'distanceRates', alternateText: 'eqwewq', }, - { - translation: translate('workspace.common.expensifyCard'), - value: 'expensifyCard', - alternateText: 'eqwewq', - }, - { - translation: translate('workspace.common.companyCards'), - value: 'companyCards', - alternateText: 'eqwewq', - }, { translation: translate('workspace.common.perDiem'), value: 'perDiem', @@ -108,7 +112,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm ]; return result; - }, [formattedAddress, policy?.name, policy?.outputCurrency, translate]); + }, [categoriesCount, formattedAddress, policy?.name, policy?.outputCurrency, taxesLength, totalMembers, totalTags, translate]); const listData: ListItem[] = useMemo(() => { return items.map((option) => ({ @@ -123,8 +127,8 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm if (!policyID) { return; } - openWorkspaceMembersPage(policyID, Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList))); - }, [policyID, policy?.employeeList]); + openWorkspaceMembersPage(policyID, Object.keys(allIds ?? {})); + }, [policyID, allIds]); const confirmDuplicateAndHideModal = useCallback(() => { setIsDuplicateModalOpen(false); @@ -133,14 +137,30 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const updateSelectedItems = useCallback( (listItem: ListItem) => { if (listItem.isSelected) { - setSelectedItems(selectedItems.filter((i) => i !== listItem.keyForList)); + if (listItem.keyForList === 'selectAll') { + setSelectedItems([]); + return; + } + setSelectedItems(selectedItems.filter((i) => i !== listItem.keyForList && i !== 'selectAll')); + return; + } + if (listItem.keyForList === 'selectAll') { + setSelectedItems(items.map((i) => i.value)); return; } const newItem = items.find((i) => i.value === listItem.keyForList)?.value; if (newItem) { - setSelectedItems([...selectedItems, newItem]); + const newSelectedItems = [...selectedItems, newItem]; + const featuresOptions = items.filter((i) => i.value !== 'selectAll'); + const allItemsSelected = featuresOptions.length === newSelectedItems.length; + + if (allItemsSelected) { + setSelectedItems([...newSelectedItems, 'selectAll']); + } else { + setSelectedItems(newSelectedItems); + } } }, [items, selectedItems], @@ -168,6 +188,9 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm ListItem={MultiSelectListItem} onSelectRow={updateSelectedItems} isAlternateTextMultilineSupported + showConfirmButton + confirmButtonText={translate('common.next')} + onConfirm={() => setIsDuplicateModalOpen(true)} /> @@ -179,7 +202,11 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm prompt={ - {translate('workspace.duplicateWorkspace.confirmTitle', {newWorkspaceName: duplicateWorkspace?.name, oldWorkspaceName: policy?.name, totalMembers})} + {translate('workspace.duplicateWorkspace.confirmTitle', { + newWorkspaceName: duplicateWorkspace?.name, + oldWorkspaceName: policy?.name, + totalMembers, + })} {translate('workspace.duplicateWorkspace.confirmDuplicate')} From db8a98166f9e72c651f6a22ce521d1173ebb389a Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 25 Jul 2025 17:56:18 +0200 Subject: [PATCH 04/29] fix: revert validating against the old name --- src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx index 0e2443126af7c..d246facf88fb1 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -54,14 +54,11 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 // code units. addErrorMessage(errors, 'name', translate('common.error.characterLimitExceedCounter', {length: [...name].length, limit: CONST.TITLE_CHARACTER_LIMIT})); - } else if (name === policy?.name) { - // TODO: add error message translation - addErrorMessage(errors, 'name', 'the new name cannot be the same as the old policy name'); } return errors; }, - [policy?.name, translate], + [translate], ); const onSubmit = useCallback(({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { From 23fb394e10075b509f00d8faa9be1a05acc83aca Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 25 Jul 2025 18:39:12 +0200 Subject: [PATCH 05/29] feat: display the option for admin, add distance rates --- src/pages/workspace/WorkspacesListPage.tsx | 71 ++++++++++--------- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 31 ++++---- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index ccf4962152c62..7b485e49b5d75 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -184,42 +184,43 @@ function WorkspacesListPage() { ]; if (isOwner) { - threeDotsMenuItems.push( - { - icon: Expensicons.Trashcan, - text: translate('workspace.common.delete'), - shouldShowLoadingSpinnerIcon: loadingSpinnerIconIndex === index, - onSelected: () => { - if (loadingSpinnerIconIndex !== null) { - return; - } - - if (isSupportalAction) { - setIsSupportalActionRestrictedModalOpen(true); - return; - } - - setPolicyIDToDelete(item.policyID); - setPolicyNameToDelete(item.title); - - if (shouldCalculateBillNewDot) { - setIsDeletingPaidWorkspace(true); - calculateBillNewDot(); - setLoadingSpinnerIconIndex(index); - return; - } - - setIsDeleteModalOpen(true); - }, - shouldKeepModalOpen: shouldCalculateBillNewDot, - shouldCallAfterModalHide: !shouldCalculateBillNewDot, - }, - { - icon: Expensicons.Copy, - text: translate('workspace.common.duplicateWorkspace'), - onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE.getRoute(item.policyID)) : undefined), + threeDotsMenuItems.push({ + icon: Expensicons.Trashcan, + text: translate('workspace.common.delete'), + shouldShowLoadingSpinnerIcon: loadingSpinnerIconIndex === index, + onSelected: () => { + if (loadingSpinnerIconIndex !== null) { + return; + } + + if (isSupportalAction) { + setIsSupportalActionRestrictedModalOpen(true); + return; + } + + setPolicyIDToDelete(item.policyID); + setPolicyNameToDelete(item.title); + + if (shouldCalculateBillNewDot) { + setIsDeletingPaidWorkspace(true); + calculateBillNewDot(); + setLoadingSpinnerIconIndex(index); + return; + } + + setIsDeleteModalOpen(true); }, - ); + shouldKeepModalOpen: shouldCalculateBillNewDot, + shouldCallAfterModalHide: !shouldCalculateBillNewDot, + }); + } + + if (isAdmin) { + threeDotsMenuItems.push({ + icon: Expensicons.Copy, + text: translate('workspace.common.duplicateWorkspace'), + onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE.getRoute(item.policyID)) : undefined), + }); } if (!(isAdmin || isOwner)) { diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index bd2bbe510c495..387d73d885369 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -11,11 +11,9 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; -import {getStatusOptions} from '@libs/SearchUIUtils'; +import {getDistanceRateCustomUnit, getMemberAccountIDsForWorkspace, getPerDiemCustomUnit} from '@libs/PolicyUtils'; import {openWorkspaceMembersPage} from '@userActions/Policy/Member'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceDuplicateFormProps = { @@ -37,6 +35,9 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const categoriesCount = Object.keys(policyCategories ?? {}).length; const [selectedItems, setSelectedItems] = useState([]); + const customUnit = getDistanceRateCustomUnit(policy); + const ratesCount = Object.keys(customUnit?.rates ?? {}).length; + const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); const formattedAddress = !isEmptyObject(policy) && !isEmptyObject(policy.address) @@ -54,11 +55,13 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm value: 'overview', alternateText: `${policy?.name}, ${policy?.outputCurrency} ${translate('common.currency')}, ${formattedAddress}`, }, - { - translation: translate('workspace.common.members'), - value: 'members', - alternateText: totalMembers ? `${totalMembers} ${translate('workspace.common.members')}` : undefined, - }, + totalMembers > 1 + ? { + translation: translate('workspace.common.members'), + value: 'members', + alternateText: totalMembers ? `${totalMembers} ${translate('workspace.common.members').toLowerCase()}` : undefined, + } + : undefined, { translation: translate('workspace.common.reports'), value: 'reports', @@ -72,17 +75,17 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm { translation: translate('workspace.common.tags'), value: 'tags', - alternateText: totalTags ? `${totalTags} ${translate('workspace.common.tags')}` : undefined, + alternateText: totalTags ? `${totalTags} ${translate('workspace.common.tags').toLowerCase()}` : undefined, }, { translation: translate('workspace.common.categories'), value: 'categories', - alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.common.categories')}` : undefined, + alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.common.categories').toLowerCase()}` : undefined, }, { translation: translate('workspace.common.taxes'), value: 'taxes', - alternateText: taxesLength ? `${taxesLength} ${translate('workspace.common.taxes')}` : undefined, + alternateText: taxesLength ? `${taxesLength} ${translate('workspace.common.taxes').toLowerCase()}` : undefined, }, { translation: translate('workspace.common.workflows'), @@ -97,7 +100,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm { translation: translate('workspace.common.distanceRates'), value: 'distanceRates', - alternateText: 'eqwewq', + alternateText: ratesCount ? `${ratesCount} ${translate('iou.rates').toLowerCase()}` : undefined, }, { translation: translate('workspace.common.perDiem'), @@ -111,8 +114,8 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm }, ]; - return result; - }, [categoriesCount, formattedAddress, policy?.name, policy?.outputCurrency, taxesLength, totalMembers, totalTags, translate]); + return result.filter((item): item is NonNullable => item !== undefined); + }, [categoriesCount, formattedAddress, policy?.name, policy?.outputCurrency, ratesCount, taxesLength, totalMembers, totalTags, translate]); const listData: ListItem[] = useMemo(() => { return items.map((option) => ({ From a6b461a1119d172791c0b653948bad1f288646e4 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 30 Jul 2025 16:28:18 +0300 Subject: [PATCH 06/29] add logic for menu items --- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + src/libs/PolicyUtils.ts | 7 ++ .../WorkspaceDuplicateSelectFeaturesForm.tsx | 113 ++++++++++++------ 12 files changed, 94 insertions(+), 36 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 6d958ce425552..6deafb9e65f19 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4828,6 +4828,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 2b806eeb6f5b4..fb1853982a9a0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4807,6 +4807,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index d2bd9f1c6df26..7de698f1f3b94 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4820,6 +4820,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 38b3cfcd6fa7d..12de1bea3bdf7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4843,6 +4843,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 62197d1239a63..7be44e90ac202 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4843,6 +4843,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c67e81835d80e..c31572ac97918 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4822,6 +4822,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a49d9fee4a88f..376adedad1153 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4845,6 +4845,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f37812f99d030..4647f802f2c89 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4834,6 +4834,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index be2cfaca850e0..91be1e4a1cc42 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4839,6 +4839,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ce434667bc46a..d33d5b53d0d46 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4761,6 +4761,7 @@ const translations = { selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', + categories: 'categories and your auto-categorization rules', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 42b0663bf5b2d..554cd0f4d8abc 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1238,6 +1238,12 @@ function getValidConnectedIntegration(policy: Policy | undefined, accountingInte ); } +function getAllValidConnectedIntegration(policy: Policy | undefined, accountingIntegrations?: ConnectionName[]) { + return (accountingIntegrations ?? Object.values(CONST.POLICY.CONNECTIONS.NAME)).filter( + (integration) => !!policy?.connections?.[integration] && !isAuthenticationError(policy, integration), + ); +} + function hasIntegrationAutoSync(policy: Policy | undefined, connectedIntegration?: ConnectionName) { return (connectedIntegration && policy?.connections?.[connectedIntegration]?.config?.autoSync?.enabled) ?? false; } @@ -1639,6 +1645,7 @@ export { getCountOfRequiredTagLists, getActiveEmployeeWorkspaces, isUserInvitedToWorkspace, + getAllValidConnectedIntegration, getPolicyRole, hasIndependentTags, }; diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 387d73d885369..7cabee9dde232 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -11,14 +11,18 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getDistanceRateCustomUnit, getMemberAccountIDsForWorkspace, getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import {getAllValidConnectedIntegration, getDistanceRateCustomUnit, getMemberAccountIDsForWorkspace, getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import {getReportFieldsByPolicyID} from '@libs/ReportUtils'; import {openWorkspaceMembersPage} from '@userActions/Policy/Member'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Rate} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; type WorkspaceDuplicateFormProps = { policyID?: string; }; +const DEFAULT_SELECT_ALL = 'selectAll'; function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateFormProps) { const styles = useThemeStyles(); @@ -31,12 +35,24 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, {canBeMissing: false}); const totalTags = Object.keys(policyTags ?? {}).length ?? 0; const taxesLength = Object.keys(policy?.taxRates?.taxes ?? {}).length ?? 0; - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true}); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, {canBeMissing: true}); const categoriesCount = Object.keys(policyCategories ?? {}).length; const [selectedItems, setSelectedItems] = useState([]); + const reportFields = Object.keys(getReportFieldsByPolicyID(policyID)).length ?? 0; + const customUnits = getPerDiemCustomUnit(policy); + const customUnitRates: Record = customUnits?.rates ?? {}; + const allRates = Object.values(customUnitRates)?.length; + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + + const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); + const connectedIntegration = getAllValidConnectedIntegration(policy, accountingIntegrations); const customUnit = getDistanceRateCustomUnit(policy); const ratesCount = Object.keys(customUnit?.rates ?? {}).length; + const invoiceCompany = + policy?.invoice?.companyName && policy?.invoice?.companyWebsite + ? `${policy?.invoice?.companyName}, ${policy?.invoice?.companyWebsite}` + : (policy?.invoice?.companyName ?? policy?.invoice?.companyWebsite); const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); const formattedAddress = @@ -48,7 +64,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const result = [ { translation: translate('workspace.common.selectAll'), - value: 'selectAll', + value: DEFAULT_SELECT_ALL, }, { translation: translate('workspace.common.profile'), @@ -62,31 +78,39 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm alternateText: totalMembers ? `${totalMembers} ${translate('workspace.common.members').toLowerCase()}` : undefined, } : undefined, - { - translation: translate('workspace.common.reports'), - value: 'reports', - alternateText: 'eqwewq', - }, - { - translation: translate('workspace.common.accounting'), - value: 'accounting', - alternateText: 'eqwewq', - }, - { - translation: translate('workspace.common.tags'), - value: 'tags', - alternateText: totalTags ? `${totalTags} ${translate('workspace.common.tags').toLowerCase()}` : undefined, - }, + reportFields > 0 + ? { + translation: translate('workspace.common.reports'), + value: 'reports', + alternateText: reportFields ? `${reportFields} ${translate('workspace.common.reportFields').toLowerCase()}` : undefined, + } + : undefined, + connectedIntegration && connectedIntegration?.length > 0 + ? { + translation: translate('workspace.common.accounting'), + value: 'accounting', + alternateText: connectedIntegration.map((connectionName) => CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]).join(', '), + } + : undefined, + totalTags > 0 + ? { + translation: translate('workspace.common.tags'), + value: 'tags', + alternateText: totalTags ? `${totalTags} ${translate('workspace.common.tags').toLowerCase()}` : undefined, + } + : undefined, { translation: translate('workspace.common.categories'), value: 'categories', - alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.common.categories').toLowerCase()}` : undefined, - }, - { - translation: translate('workspace.common.taxes'), - value: 'taxes', - alternateText: taxesLength ? `${taxesLength} ${translate('workspace.common.taxes').toLowerCase()}` : undefined, + alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.duplicateWorkspace.categories').toLowerCase()}` : undefined, }, + taxesLength > 0 + ? { + translation: translate('workspace.common.taxes'), + value: 'taxes', + alternateText: taxesLength ? `${taxesLength} ${translate('workspace.common.taxes').toLowerCase()}` : undefined, + } + : undefined, { translation: translate('workspace.common.workflows'), value: 'workflows', @@ -102,20 +126,37 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm value: 'distanceRates', alternateText: ratesCount ? `${ratesCount} ${translate('iou.rates').toLowerCase()}` : undefined, }, - { - translation: translate('workspace.common.perDiem'), - value: 'perDiem', - alternateText: 'eqwewq', - }, + allRates > 0 + ? { + translation: translate('workspace.common.perDiem'), + value: 'perDiem', + alternateText: allRates ? `${allRates}${translate('workspace.common.perDiem').toLowerCase()}` : undefined, + } + : undefined, { translation: translate('workspace.common.invoices'), value: 'invoices', - alternateText: 'eqwewq', + alternateText: bankAccountList ? `${Object.keys(bankAccountList).length} ${translate('common.bankAccounts').toLowerCase()}, ${invoiceCompany}` : invoiceCompany, }, ]; return result.filter((item): item is NonNullable => item !== undefined); - }, [categoriesCount, formattedAddress, policy?.name, policy?.outputCurrency, ratesCount, taxesLength, totalMembers, totalTags, translate]); + }, [ + translate, + policy?.name, + policy?.outputCurrency, + formattedAddress, + totalMembers, + reportFields, + connectedIntegration, + totalTags, + categoriesCount, + taxesLength, + ratesCount, + allRates, + bankAccountList, + invoiceCompany, + ]); const listData: ListItem[] = useMemo(() => { return items.map((option) => ({ @@ -140,14 +181,14 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const updateSelectedItems = useCallback( (listItem: ListItem) => { if (listItem.isSelected) { - if (listItem.keyForList === 'selectAll') { + if (listItem.keyForList === DEFAULT_SELECT_ALL) { setSelectedItems([]); return; } - setSelectedItems(selectedItems.filter((i) => i !== listItem.keyForList && i !== 'selectAll')); + setSelectedItems(selectedItems.filter((i) => i !== listItem.keyForList && i !== DEFAULT_SELECT_ALL)); return; } - if (listItem.keyForList === 'selectAll') { + if (listItem.keyForList === DEFAULT_SELECT_ALL) { setSelectedItems(items.map((i) => i.value)); return; } @@ -156,11 +197,11 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm if (newItem) { const newSelectedItems = [...selectedItems, newItem]; - const featuresOptions = items.filter((i) => i.value !== 'selectAll'); + const featuresOptions = items.filter((i) => i.value !== DEFAULT_SELECT_ALL); const allItemsSelected = featuresOptions.length === newSelectedItems.length; if (allItemsSelected) { - setSelectedItems([...newSelectedItems, 'selectAll']); + setSelectedItems([...newSelectedItems, DEFAULT_SELECT_ALL]); } else { setSelectedItems(newSelectedItems); } From 0fbd9a11a39b6fcf039589ce42411bbff5293f6c Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 31 Jul 2025 13:17:28 +0300 Subject: [PATCH 07/29] add logic for rules --- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 17 ++++--- src/pages/workspace/duplicate/utils.ts | 44 +++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 src/pages/workspace/duplicate/utils.ts diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 7cabee9dde232..085d4b848d9d9 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -18,6 +18,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Rate} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import {getWorkspaceRules} from './utils'; type WorkspaceDuplicateFormProps = { policyID?: string; @@ -43,6 +44,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const customUnitRates: Record = customUnits?.rates ?? {}; const allRates = Object.values(customUnitRates)?.length; const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + const rules = getWorkspaceRules(policy, translate); const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); const connectedIntegration = getAllValidConnectedIntegration(policy, accountingIntegrations); @@ -116,11 +118,15 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm value: 'workflows', alternateText: 'eqwewq', }, - { - translation: translate('workspace.common.rules'), - value: 'rules', - alternateText: 'eqwewq', - }, + rules && rules.length > 0 + ? { + translation: translate('workspace.common.rules'), + value: 'rules', + alternateText: rules.length + ? `${rules.length} ${translate('workspace.common.workspace').toLowerCase()} ${translate('workspace.common.rules').toLowerCase()}: ${rules.join(', ')}` + : undefined, + } + : undefined, { translation: translate('workspace.common.distanceRates'), value: 'distanceRates', @@ -152,6 +158,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm totalTags, categoriesCount, taxesLength, + rules, ratesCount, allRates, bankAccountList, diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts new file mode 100644 index 0000000000000..97c51379c1549 --- /dev/null +++ b/src/pages/workspace/duplicate/utils.ts @@ -0,0 +1,44 @@ +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import {getWorkflowApprovalsUnavailable, hasVBBA} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import type {Policy} from '@src/types/onyx'; + +function getWorkspaceRules(policy: Policy, translate: LocaleContextProps['translate']) { + const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); + const autoPayApprovedReportsUnavailable = !policy?.areWorkflowsEnabled || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES || !hasVBBA(policy?.id); + const total: string[] = []; + if (policy?.maxExpenseAmountNoReceipt !== CONST.DISABLED_MAX_EXPENSE_VALUE) { + total.push(translate('workspace.rules.individualExpenseRules.receiptRequiredAmount')); + } + if (policy?.maxExpenseAmount !== CONST.DISABLED_MAX_EXPENSE_VALUE) { + total.push(translate('workspace.rules.individualExpenseRules.maxExpenseAmount')); + } + if (policy?.maxExpenseAge !== CONST.DISABLED_MAX_EXPENSE_VALUE) { + total.push(translate('workspace.rules.individualExpenseRules.maxExpenseAge')); + } + if (policy?.defaultBillable) { + total.push(translate('workspace.rules.individualExpenseRules.billable')); + } + if (policy?.prohibitedExpenses && Object.values(policy?.prohibitedExpenses).find((value) => value)) { + total.push(translate('workspace.rules.individualExpenseRules.prohibitedExpenses')); + } + if (policy?.eReceipts) { + total.push(translate('workspace.rules.individualExpenseRules.eReceipts')); + } + if (policy?.isAttendeeTrackingEnabled) { + total.push(translate('workspace.rules.individualExpenseRules.attendeeTracking')); + } + if (policy?.preventSelfApproval && !workflowApprovalsUnavailable) { + total.push(translate('workspace.rules.expenseReportRules.preventSelfApprovalsTitle')); + } + if (policy?.shouldShowAutoApprovalOptions && !workflowApprovalsUnavailable) { + total.push(translate('workspace.rules.expenseReportRules.autoApproveCompliantReportsTitle')); + } + if (policy?.shouldShowAutoReimbursementLimitOption && !autoPayApprovedReportsUnavailable) { + total.push(translate('workspace.rules.expenseReportRules.autoPayApprovedReportsTitle')); + } + + return total.length > 0 ? total : null; +} + +export {getWorkspaceRules}; From acc70aed5338d51b741cd96b586ddb628823b0f2 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 1 Aug 2025 14:53:18 +0300 Subject: [PATCH 08/29] workflows logic --- src/languages/de.ts | 2 ++ src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/languages/fr.ts | 2 ++ src/languages/it.ts | 2 ++ src/languages/ja.ts | 2 ++ src/languages/nl.ts | 2 ++ src/languages/pl.ts | 2 ++ src/languages/pt-BR.ts | 2 ++ src/languages/zh-hans.ts | 2 ++ .../ModalStackNavigators/index.tsx | 3 +- src/libs/actions/Policy/Policy.ts | 3 +- .../duplicate/WorkspaceDuplicateForm.tsx | 17 ++++++---- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 22 +++++++------ src/pages/workspace/duplicate/utils.ts | 33 +++++++++++++++++-- src/types/onyx/DuplicateWorkspace.ts | 4 +-- src/types/onyx/index.ts | 2 +- 17 files changed, 78 insertions(+), 26 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 6deafb9e65f19..5623cd47d06f4 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4829,6 +4829,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index fb1853982a9a0..3e38753c07f63 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4808,6 +4808,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 7de698f1f3b94..e26be406f6530 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4821,6 +4821,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 12de1bea3bdf7..729751fd0dc23 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4844,6 +4844,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 7be44e90ac202..9bf5aa9e4c31f 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4844,6 +4844,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c31572ac97918..b96baa8329e89 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4823,6 +4823,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 376adedad1153..cbb2de238881b 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4846,6 +4846,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 4647f802f2c89..05f052883f7a8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4835,6 +4835,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 91be1e4a1cc42..d3253c3b9ffb8 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4840,6 +4840,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d33d5b53d0d46..919238c1e66bc 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4762,6 +4762,8 @@ const translations = { whichFeatures: 'Which features do you want to copy over to your new workspace?', confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', + reimbursementAccount: 'reimbursement account', + delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 47b036617c38a..3922eb2da3c0d 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -38,7 +38,8 @@ import type { TransactionDuplicateNavigatorParamList, TravelNavigatorParamList, WalletStatementNavigatorParamList, - WorkspaceConfirmationNavigatorParamList, WorkspaceDuplicateNavigatorParamList, + WorkspaceConfirmationNavigatorParamList, + WorkspaceDuplicateNavigatorParamList, } from '@navigation/types'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index a6034e111a110..73d2b4ef6f912 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -89,6 +89,7 @@ import CONST from '@src/CONST'; import type {OnboardingAccounting} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type { + DuplicateWorkspace, IntroSelected, InvitedEmailsToAccountIDs, PersonalDetailsList, @@ -104,13 +105,11 @@ import type { Transaction, TransactionViolations, } from '@src/types/onyx'; -import type {AddNewCardFeedData} from '@src/types/onyx/CardFeeds'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {Attributes, CompanyAddress, CustomUnit, NetSuiteCustomList, NetSuiteCustomSegment, ProhibitedExpenses, Rate, TaxRate} from '@src/types/onyx/Policy'; import type {CustomFieldType} from '@src/types/onyx/PolicyEmployee'; import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import DuplicateWorkspace from '../../../types/onyx/DuplicateWorkspace'; import {buildOptimisticMccGroup, buildOptimisticPolicyCategories} from './Category'; type ReportCreationData = Record< diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx index d246facf88fb1..67b9f22d41652 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -61,13 +61,16 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { [translate], ); - const onSubmit = useCallback(({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { - const newPolicyID = generatePolicyID(); - setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); - if (policyID) { - Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); - } - }, [policyID]); + const onSubmit = useCallback( + ({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { + const newPolicyID = generatePolicyID(); + setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); + if (policyID) { + Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); + } + }, + [policyID], + ); const [workspaceNameFirstCharacter, setWorkspaceNameFirstCharacter] = useState(defaultWorkspaceName ?? ''); diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 085d4b848d9d9..55f9a09f97d32 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -18,7 +18,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Rate} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getWorkspaceRules} from './utils'; +import {getWorkflowRules, getWorkspaceRules} from './utils'; type WorkspaceDuplicateFormProps = { policyID?: string; @@ -44,7 +44,6 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const customUnitRates: Record = customUnits?.rates ?? {}; const allRates = Object.values(customUnitRates)?.length; const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); - const rules = getWorkspaceRules(policy, translate); const accountingIntegrations = Object.values(CONST.POLICY.CONNECTIONS.NAME); const connectedIntegration = getAllValidConnectedIntegration(policy, accountingIntegrations); @@ -63,6 +62,9 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm : ''; const items = useMemo(() => { + const rules = getWorkspaceRules(policy, translate); + const workflows = getWorkflowRules(policy, translate); + const result = [ { translation: translate('workspace.common.selectAll'), @@ -113,11 +115,13 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm alternateText: taxesLength ? `${taxesLength} ${translate('workspace.common.taxes').toLowerCase()}` : undefined, } : undefined, - { - translation: translate('workspace.common.workflows'), - value: 'workflows', - alternateText: 'eqwewq', - }, + workflows && workflows?.length > 0 + ? { + translation: translate('workspace.common.workflows'), + value: 'workflows', + alternateText: workflows?.join(', '), + } + : undefined, rules && rules.length > 0 ? { translation: translate('workspace.common.rules'), @@ -148,9 +152,8 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm return result.filter((item): item is NonNullable => item !== undefined); }, [ + policy, translate, - policy?.name, - policy?.outputCurrency, formattedAddress, totalMembers, reportFields, @@ -158,7 +161,6 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm totalTags, categoriesCount, taxesLength, - rules, ratesCount, allRates, bankAccountList, diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts index 97c51379c1549..05a5985930c81 100644 --- a/src/pages/workspace/duplicate/utils.ts +++ b/src/pages/workspace/duplicate/utils.ts @@ -1,9 +1,11 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import {getWorkflowApprovalsUnavailable, hasVBBA} from '@libs/PolicyUtils'; +import {getCorrectedAutoReportingFrequency, getWorkflowApprovalsUnavailable, hasVBBA} from '@libs/PolicyUtils'; +import {getAutoReportingFrequencyDisplayNames} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; +import type {AutoReportingFrequencyKey} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; import CONST from '@src/CONST'; import type {Policy} from '@src/types/onyx'; -function getWorkspaceRules(policy: Policy, translate: LocaleContextProps['translate']) { +function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextProps['translate']) { const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); const autoPayApprovedReportsUnavailable = !policy?.areWorkflowsEnabled || policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES || !hasVBBA(policy?.id); const total: string[] = []; @@ -41,4 +43,29 @@ function getWorkspaceRules(policy: Policy, translate: LocaleContextProps['transl return total.length > 0 ? total : null; } -export {getWorkspaceRules}; +function getWorkflowRules(policy: Policy | undefined, translate: LocaleContextProps['translate']) { + const total: string[] = []; + const {bankAccountID} = policy?.achAccount ?? {}; + const hasDelayedSubmissionError = !!(policy?.errorFields?.autoReporting ?? policy?.errorFields?.autoReportingFrequency); + const hasApprovalError = !!policy?.errorFields?.approvalMode; + const shouldShowBankAccount = !!bankAccountID && policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; + + if (policy?.autoReportingFrequency !== CONST.POLICY.AUTO_REPORTING_FREQUENCIES.INSTANT && !hasDelayedSubmissionError) { + const title = + getAutoReportingFrequencyDisplayNames(translate)[(getCorrectedAutoReportingFrequency(policy) as AutoReportingFrequencyKey) ?? CONST.POLICY.AUTO_REPORTING_FREQUENCIES.WEEKLY]; + total.push(`${title} ${translate('workspace.duplicateWorkspace.delayedSubmission')}`); + } + if ([CONST.POLICY.APPROVAL_MODE.BASIC, CONST.POLICY.APPROVAL_MODE.ADVANCED].some((approvalMode) => approvalMode === policy?.approvalMode) && !hasApprovalError) { + total.push(translate('common.approvals')); + } + if (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO) { + if (shouldShowBankAccount) { + total.push(`1 ${translate('workspace.duplicateWorkspace.reimbursementAccount')}`); + } else { + total.push(translate('common.payments')); + } + } + return total.length > 0 ? total : null; +} + +export {getWorkspaceRules, getWorkflowRules}; diff --git a/src/types/onyx/DuplicateWorkspace.ts b/src/types/onyx/DuplicateWorkspace.ts index 2ee68beb6a208..453cdfb441a59 100644 --- a/src/types/onyx/DuplicateWorkspace.ts +++ b/src/types/onyx/DuplicateWorkspace.ts @@ -1,4 +1,4 @@ -import type {CustomRNImageManipulatorResult} from "@libs/cropOrRotateImage/types"; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import type * as OnyxCommon from './OnyxCommon'; /** Model of plaid data */ @@ -10,7 +10,7 @@ type DuplicateWorkspace = { name?: string; /** Workspace avatar */ - file?: File | CustomRNImageManipulatorResult + file?: File | CustomRNImageManipulatorResult; /** Whether the data is being fetched from server */ isLoading?: boolean; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 2914551ac503f..212ce6282927c 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -28,6 +28,7 @@ import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; import type Download from './Download'; import type DraftReportComments from './DraftReportComments'; +import type DuplicateWorkspace from './DuplicateWorkspace'; import type ExpensifyCardBankAccountMetadata from './ExpensifyCardBankAccountMetadata'; import type ExpensifyCardSettings from './ExpensifyCardSettings'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; @@ -59,7 +60,6 @@ import type PersonalBankAccount from './PersonalBankAccount'; import type {PersonalDetailsList, PersonalDetailsMetadata} from './PersonalDetails'; import type PersonalDetails from './PersonalDetails'; import type PlaidData from './PlaidData'; -import type DuplicateWorkspace from './DuplicateWorkspace'; import type Policy from './Policy'; import type {PolicyConnectionName, PolicyConnectionSyncProgress, PolicyReportField, TaxRate, TaxRates, TaxRatesWithDefault} from './Policy'; import type {PolicyCategories, PolicyCategory} from './PolicyCategory'; From 3cb2ac24a39134e9837320b1f04bcd20a26361bd Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 5 Aug 2025 11:39:21 +0300 Subject: [PATCH 09/29] connection to old dot api --- ios/Podfile.lock | 30 +- src/languages/de.ts | 1 + src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + .../parameters/DuplicateWorkspaceParams.ts | 19 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 1 + src/libs/actions/Policy/Policy.ts | 267 +++++++++++++++++- src/pages/workspace/WorkspacesListPage.tsx | 1 + src/pages/workspace/WorkspacesListRow.tsx | 19 +- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 86 ++++-- src/types/onyx/DuplicateWorkspace.ts | 3 +- 19 files changed, 396 insertions(+), 41 deletions(-) create mode 100644 src/libs/API/parameters/DuplicateWorkspaceParams.ts diff --git a/ios/Podfile.lock b/ios/Podfile.lock index e401c731e4aff..121f790fd7b0b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2873,7 +2873,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.294): + - RNLiveMarkdown (0.1.298): - DoubleConversion - glog - hermes-engine @@ -3003,7 +3003,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.17.1): + - RNReanimated (3.19.0): - DoubleConversion - glog - hermes-engine @@ -3026,10 +3026,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.17.1) - - RNReanimated/worklets (= 3.17.1) + - RNReanimated/reanimated (= 3.19.0) + - RNReanimated/worklets (= 3.19.0) - Yoga - - RNReanimated/reanimated (3.17.1): + - RNReanimated/reanimated (3.19.0): - DoubleConversion - glog - hermes-engine @@ -3052,9 +3052,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.17.1) + - RNReanimated/reanimated/apple (= 3.19.0) - Yoga - - RNReanimated/reanimated/apple (3.17.1): + - RNReanimated/reanimated/apple (3.19.0): - DoubleConversion - glog - hermes-engine @@ -3078,7 +3078,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.17.1): + - RNReanimated/worklets (3.19.0): - DoubleConversion - glog - hermes-engine @@ -3101,9 +3101,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/worklets/apple (= 3.17.1) + - RNReanimated/worklets/apple (= 3.19.0) - Yoga - - RNReanimated/worklets/apple (3.17.1): + - RNReanimated/worklets/apple (3.19.0): - DoubleConversion - glog - hermes-engine @@ -3780,7 +3780,7 @@ SPEC CHECKSUMS: ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: 3bf0840584001b8fba01e90f9f660db43025521d fullstory_react-native: eea992f5f335d6b73cb5c3cc06942660388ef92f - glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 + glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db @@ -3849,7 +3849,7 @@ SPEC CHECKSUMS: react-native-geolocation: d5114c4ea019574f76344eca8503555ba1208b46 react-native-image-picker: 05b58a33b780623da2f891c55dcc57c6aa867d83 react-native-key-command: b04997038f8fe4a03b711abe36cd84ced93c50b3 - react-native-keyboard-controller: 2602bca1715435e3742c5a69d45ff42c1d97e1b9 + react-native-keyboard-controller: bf564ee7ea1a9b4681aa83989a06f3990b4291a1 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: b8577cba87fbe7b7740c279ee23d25d7c5d87666 react-native-pager-view: 12c1b7b6f50efb3e5d57d62ea5c601cba0585bfd @@ -3905,13 +3905,13 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: b249b5be5a3659025aed8898aaaafd567dc2d660 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: a2f598d3a4a025543185c2b5aa6ab86e9dfaaf8c + RNLiveMarkdown: 737ec283c8c639885884a04ed6836c2878e14a19 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: a5569b64e3ca5fcd1be1f9d5bb478bf601d2a033 RNNitroSQLite: a1b0f1a665c54fc0f2510a292c1bf8ac4d44231b RNPermissions: fd6b2676e74ecb6d2dec0a6168502ab7af733e34 RNReactNativeHapticFeedback: 85c0a6ff490d52f5e8073040296fefe5945ebbfa - RNReanimated: 71c3cc66ee4866e550ddc202e01cd670de3b9ed5 + RNReanimated: 5794e936ec892ebd4a0143cbcd5ff0e9ecb1573d RNScreens: d9d5d8a2a484bb4446968bfa00db991f1117db44 RNShare: 1e3e15a3d2608acde2808bc35448e2344e38e15b RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 @@ -3928,4 +3928,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f7a8d931f2dd2be0fed9bb22df37140219a07ff3 -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/src/languages/de.ts b/src/languages/de.ts index abbc846291ef7..08c24533ebe51 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4843,6 +4843,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b6a359c5f89ea..17c559bec68ec 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4821,6 +4821,7 @@ const translations = { confirmDuplicate: 'Are you sure you want to proceed?', categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', + welcomeNote: 'Please start using my new workspace', delayedSubmission: 'delayed submission', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 4b4aec7e6b0b2..e2f532be4d9f2 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4836,6 +4836,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a61cfd5179bb8..fc542f283d7ac 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4860,6 +4860,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index bb62e60bf549f..28fd148edecca 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4859,6 +4859,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4ad790865d0c8..a7dd0489b1233 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4838,6 +4838,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index acef9de289c94..8dbd9e25a31ff 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4861,6 +4861,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 93723f81417e9..64fd79cc848a3 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4850,6 +4850,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 78f53beece40c..1034cbf6caa9e 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4854,6 +4854,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4ba2061819bb5..32c3bb580ec62 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4776,6 +4776,7 @@ const translations = { categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', delayedSubmission: 'delayed submission', + welcomeNote: 'Please start using my new workspace', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, }, diff --git a/src/libs/API/parameters/DuplicateWorkspaceParams.ts b/src/libs/API/parameters/DuplicateWorkspaceParams.ts new file mode 100644 index 0000000000000..13aa985221dd7 --- /dev/null +++ b/src/libs/API/parameters/DuplicateWorkspaceParams.ts @@ -0,0 +1,19 @@ +import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; + +type DuplicateWorkspaceParams = { + policyID: string; + policyName: string; + parts: Record; + announceChatReportID: string; + adminsChatReportID: string; + welcomeNote: string; + expenseChatReportID: string; + adminsCreatedReportActionID: string; + expenseCreatedReportActionID: string; + announceChatReportActionID: string; + customUnitID: string; + customUnitRateID: string; + file?: FileObject; +}; + +export default DuplicateWorkspaceParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index ac632b550a163..802e0280d1ac9 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -76,6 +76,7 @@ export type {default as SignInWithSupportAuthTokenParams} from './SignInWithSupp export type {default as UnlinkLoginParams} from './UnlinkLoginParams'; export type {default as UpdateAutomaticTimezoneParams} from './UpdateAutomaticTimezoneParams'; export type {default as UpdateChatPriorityModeParams} from './UpdateChatPriorityModeParams'; +export type {default as DuplicateWorkspaceParams} from './DuplicateWorkspaceParams'; export type {default as UpdateDateOfBirthParams} from './UpdateDateOfBirthParams'; export type {default as UpdateDisplayNameParams} from './UpdateDisplayNameParams'; export type {default as UpdateChatNameParams} from './UpdateChatNameParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c4d096d30276e..f48d31519942f 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -144,6 +144,7 @@ const WRITE_COMMANDS = { UPDATE_WORKSPACE_DESCRIPTION: 'UpdateWorkspaceDescription', UPDATE_WORKSPACE_MEMBERS_ROLE: 'UpdateWorkspaceMembersRole', CREATE_WORKSPACE: 'CreateWorkspace', + DUPLICATE_POLICY: 'Policy_Copy', CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', UPDATE_POLICY_MEMBERS_CUSTOM_FIELDS: 'UpdatePolicyMembersCustomFields', SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 4a3f892714686..bc345ac7d4358 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -15,6 +15,7 @@ import type { DeleteWorkspaceParams, DisablePolicyBillableModeParams, DowngradeToTeamParams, + DuplicateWorkspaceParams, EnablePolicyAutoApprovalOptionsParams, EnablePolicyAutoReimbursementLimitParams, EnablePolicyCompanyCardsParams, @@ -75,7 +76,7 @@ import * as NumberUtils from '@libs/NumberUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; import * as PolicyUtils from '@libs/PolicyUtils'; -import {goBackWhenEnableFeature, isControlPolicy, navigateToExpensifyCardPage} from '@libs/PolicyUtils'; +import {getMemberAccountIDsForWorkspace, goBackWhenEnableFeature, isControlPolicy, navigateToExpensifyCardPage} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import type {PolicySelector} from '@pages/home/sidebar/FloatingActionButtonAndPopover'; import type {Feature} from '@pages/OnboardingInterestedFeatures/types'; @@ -159,6 +160,14 @@ type BuildPolicyDataOptions = { lastUsedPaymentMethod?: LastPaymentMethodType; }; +type DuplicatePolicyDataOptions = { + policyName: string; + policyID?: string; + welcomeNote: string; + parts: Record; + file?: File; +}; + const allPolicies: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.POLICY, @@ -2465,6 +2474,261 @@ function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policy return params; } +function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOptions) { + const {policyName = '', policyID = generatePolicyID(), file, welcomeNote, parts} = options; + const {customUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(policy?.outputCurrency); + + const { + adminsChatReportID, + adminsChatData, + adminsReportActionData, + adminsCreatedReportActionID, + expenseChatReportID, + expenseChatData, + expenseReportActionData, + expenseCreatedReportActionID, + pendingChatMembers, + } = ReportUtils.buildOptimisticWorkspaceChats(policyID, policyName); + const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + + const optimisticAnnounceChat = ReportUtils.buildOptimisticAnnounceChat(policyID, [...policyMemberAccountIDs]); + const announceRoomChat = optimisticAnnounceChat.announceChatData; + + const optimisticCategoriesData = buildOptimisticPolicyCategories(policyID, Object.values(CONST.POLICY.DEFAULT_CATEGORIES)); + + // WARNING: The data below should be kept in sync with the API so we create the policy with the correct configuration. + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + ...policy, + id: policyID, + name: policyName, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + pendingFields: { + autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + name: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + outputCurrency: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + address: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + areReportFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + avatarURL: file?.uri, + originalFileName: file?.name, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${expenseChatReportID}`, + value: { + isOptimisticReport: true, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...adminsChatData, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${adminsChatReportID}`, + value: { + pendingChatMembers, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, + value: adminsReportActionData, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + ...expenseChatData, + }, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: expenseReportActionData, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${expenseChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_DRAFT}${adminsChatReportID}`, + value: null, + }, + ...announceRoomChat.onyxOptimisticData, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingAction: null, + pendingFields: { + autoReporting: null, + approvalMode: null, + reimbursementChoice: null, + name: null, + outputCurrency: null, + address: null, + description: null, + type: null, + areReportFieldsEnabled: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${adminsChatReportID}`, + value: { + isOptimisticReport: false, + pendingChatMembers: [], + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, + value: { + [adminsCreatedReportActionID]: { + pendingAction: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: { + pendingFields: { + addWorkspaceRoom: null, + }, + pendingAction: null, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${expenseChatReportID}`, + value: { + isOptimisticReport: false, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: { + [expenseCreatedReportActionID]: { + pendingAction: null, + }, + }, + }, + ...announceRoomChat.onyxSuccessData, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: {employeeList: null}, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${adminsChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${adminsChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseChatReportID}`, + value: null, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`, + value: null, + }, + ...announceRoomChat.onyxFailureData, + ]; + + if (optimisticCategoriesData.optimisticData) { + optimisticData.push(...optimisticCategoriesData.optimisticData); + } + + if (optimisticCategoriesData.failureData) { + failureData.push(...optimisticCategoriesData.failureData); + } + + if (optimisticCategoriesData.successData) { + successData.push(...optimisticCategoriesData.successData); + } + + // We need to clone the file to prevent non-indexable errors. + const clonedFile = file ? createFile(file) : undefined; + + const params: DuplicateWorkspaceParams = { + policyID: policy.id, + adminsChatReportID, + expenseChatReportID, + policyName, + adminsCreatedReportActionID, + expenseCreatedReportActionID, + announceChatReportID: optimisticAnnounceChat.announceChatReportID, + announceChatReportActionID: optimisticAnnounceChat.announceChatReportActionID, + customUnitID, + parts, + welcomeNote, + customUnitRateID, + file: clonedFile, + }; + + return {successData, optimisticData, failureData, params}; +} + +function duplicateWorkspace(policy: Policy, options: DuplicatePolicyDataOptions): DuplicateWorkspaceParams { + const {optimisticData, failureData, successData, params} = buildDuplicatePolicyData(policy, options); + + API.write(WRITE_COMMANDS.DUPLICATE_POLICY, params, {optimisticData, successData, failureData}); + + return params; +} + function openPolicyWorkflowsPage(policyID: string) { if (!policyID) { Log.warn('openPolicyWorkflowsPage invalid params', {policyID}); @@ -5697,6 +5961,7 @@ export { clearBillingReceiptDetailsErrors, clearQuickbooksOnlineAutoSyncErrorField, setIsForcedToChangeCurrency, + duplicateWorkspace, setIsComingFromGlobalReimbursementsFlow, setPolicyAttendeeTrackingEnabled, updateInterestedFeatures, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 9b24629ae2a36..706a007bc07d2 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -264,6 +264,7 @@ function WorkspacesListPage() { workspaceIcon={item.icon} ownerAccountID={item.ownerAccountID} workspaceType={item.type} + shouldAnimateInHighlight={item.shouldAnimateInHighlight} isJoinRequestPending={item?.isJoinRequestPending} rowStyles={hovered && styles.hoveredComponentBG} layoutWidth={isLessThanMediumScreen ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE} diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 1d4b0b9b49526..d464bdcd85dad 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -2,6 +2,7 @@ import {Str} from 'expensify-common'; import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; +import Animated from 'react-native-reanimated'; import type {ValueOf} from 'type-fest'; import Avatar from '@components/Avatar'; import Badge from '@components/Badge'; @@ -16,6 +17,7 @@ import Tooltip from '@components/Tooltip'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import WorkspacesListRowDisplayName from '@components/WorkspacesListRowDisplayName'; +import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; @@ -23,9 +25,9 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getDisplayNameOrDefault, getPersonalDetailsByIDs} from '@libs/PersonalDetailsUtils'; import {getUserFriendlyWorkspaceType} from '@libs/PolicyUtils'; import type {AvatarSource} from '@libs/UserUtils'; -import type {AnchorPosition} from '@styles/index'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import type {AnchorPosition} from '@src/styles'; import type IconAsset from '@src/types/utils/IconAsset'; type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { @@ -75,6 +77,9 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { /** Whether the bill is loading */ isLoadingBill?: boolean; + /** Whether the list item is highlighted */ + shouldAnimateInHighlight?: boolean; + /** Function to reset loading spinner icon index */ resetLoadingSpinnerIconIndex?: () => void; }; @@ -117,6 +122,7 @@ function WorkspacesListRow({ rowStyles, style, brickRoadIndicator, + shouldAnimateInHighlight, shouldDisableThreeDotsMenu, isJoinRequestPending, policyID, @@ -128,9 +134,16 @@ function WorkspacesListRow({ const {translate} = useLocalize(); const threeDotsMenuContainerRef = useRef(null); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const theme = useTheme(); const ownerDetails = ownerAccountID && getPersonalDetailsByIDs({accountIDs: [ownerAccountID], currentUserAccountID: currentUserPersonalDetails.accountID}).at(0); const threeDotsMenuRef = useRef<{hidePopoverMenu: () => void; isPopupMenuVisible: boolean}>(null); + const animatedHighlightStyle = useAnimatedHighlightStyle({ + borderRadius: variables.componentBorderRadius, + shouldHighlight: !!shouldAnimateInHighlight, + highlightColor: theme.messageHighlightBG, + backgroundColor: theme.highlightBG, + }); useEffect(() => { if (isLoadingBill) { @@ -218,7 +231,7 @@ function WorkspacesListRow({ ); return ( - + @@ -291,7 +304,7 @@ function WorkspacesListRow({ {!shouldUseNarrowLayout && ThreeDotMenuOrPendingIcon} - + ); } diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 55f9a09f97d32..161c80a0b92c1 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -13,7 +13,14 @@ import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import {getAllValidConnectedIntegration, getDistanceRateCustomUnit, getMemberAccountIDsForWorkspace, getPerDiemCustomUnit} from '@libs/PolicyUtils'; import {getReportFieldsByPolicyID} from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import {openPolicyCategoriesPage} from '@userActions/Policy/Category'; +import {openPolicyDistanceRatesPage} from '@userActions/Policy/DistanceRate'; import {openWorkspaceMembersPage} from '@userActions/Policy/Member'; +import {openPolicyPerDiemPage} from '@userActions/Policy/PerDiem'; +import {duplicateWorkspace as duplicateWorkspaceAction, openPolicyTaxesPage, openPolicyWorkflowsPage} from '@userActions/Policy/Policy'; +import {openPolicyReportFieldsPage} from '@userActions/Policy/ReportField'; +import {openPolicyTagsPage} from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Rate} from '@src/types/onyx/Policy'; @@ -53,7 +60,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const invoiceCompany = policy?.invoice?.companyName && policy?.invoice?.companyWebsite ? `${policy?.invoice?.companyName}, ${policy?.invoice?.companyWebsite}` - : (policy?.invoice?.companyName ?? policy?.invoice?.companyWebsite); + : (policy?.invoice?.companyName ?? policy?.invoice?.companyWebsite ?? ''); const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); const formattedAddress = @@ -103,11 +110,13 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm alternateText: totalTags ? `${totalTags} ${translate('workspace.common.tags').toLowerCase()}` : undefined, } : undefined, - { - translation: translate('workspace.common.categories'), - value: 'categories', - alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.duplicateWorkspace.categories').toLowerCase()}` : undefined, - }, + categoriesCount > 0 + ? { + translation: translate('workspace.common.categories'), + value: 'categories', + alternateText: categoriesCount ? `${categoriesCount} ${translate('workspace.duplicateWorkspace.categories').toLowerCase()}` : undefined, + } + : undefined, taxesLength > 0 ? { translation: translate('workspace.common.taxes'), @@ -131,11 +140,13 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm : undefined, } : undefined, - { - translation: translate('workspace.common.distanceRates'), - value: 'distanceRates', - alternateText: ratesCount ? `${ratesCount} ${translate('iou.rates').toLowerCase()}` : undefined, - }, + ratesCount > 0 + ? { + translation: translate('workspace.common.distanceRates'), + value: 'distanceRates', + alternateText: ratesCount ? `${ratesCount} ${translate('iou.rates').toLowerCase()}` : undefined, + } + : undefined, allRates > 0 ? { translation: translate('workspace.common.perDiem'), @@ -143,11 +154,14 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm alternateText: allRates ? `${allRates}${translate('workspace.common.perDiem').toLowerCase()}` : undefined, } : undefined, - { - translation: translate('workspace.common.invoices'), - value: 'invoices', - alternateText: bankAccountList ? `${Object.keys(bankAccountList).length} ${translate('common.bankAccounts').toLowerCase()}, ${invoiceCompany}` : invoiceCompany, - }, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (bankAccountList && Object.keys(bankAccountList).length) || !!invoiceCompany + ? { + translation: translate('workspace.common.invoices'), + value: 'invoices', + alternateText: bankAccountList ? `${Object.keys(bankAccountList).length} ${translate('common.bankAccounts').toLowerCase()}, ${invoiceCompany}` : invoiceCompany, + } + : undefined, ]; return result.filter((item): item is NonNullable => item !== undefined); @@ -176,16 +190,46 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm })); }, [items, selectedItems]); - const getWorkspaceMembers = useCallback(() => { + const fetchWorkspaceRelatedData = useCallback(() => { if (!policyID) { return; } openWorkspaceMembersPage(policyID, Object.keys(allIds ?? {})); + openPolicyCategoriesPage(policyID); + openPolicyDistanceRatesPage(policyID); + openPolicyPerDiemPage(policyID); + openPolicyReportFieldsPage(policyID); + openPolicyTagsPage(policyID); + openPolicyTaxesPage(policyID); + openPolicyWorkflowsPage(policyID); }, [policyID, allIds]); const confirmDuplicateAndHideModal = useCallback(() => { setIsDuplicateModalOpen(false); - }, []); + if (!policy || !duplicateWorkspace?.name || !duplicateWorkspace?.policyID) { + return; + } + duplicateWorkspaceAction(policy, { + policyName: duplicateWorkspace.name, + policyID: duplicateWorkspace.policyID, + welcomeNote: `${translate('workspace.duplicateWorkspace.welcomeNote')} ${duplicateWorkspace.name}`, + parts: { + people: selectedItems.includes('members'), + reports: selectedItems.includes('reports'), + connections: selectedItems.includes('accounting'), + categories: selectedItems.includes('categories'), + tags: selectedItems.includes('tags'), + taxes: selectedItems.includes('taxes'), + reimbursements: selectedItems.includes('invoices'), + expenses: selectedItems.includes('reports'), + customUnits: selectedItems.includes('distanceRates'), + invoices: selectedItems.includes('invoices'), + exportLayouts: selectedItems.includes('workflows'), + }, + file: duplicateWorkspace?.file, + }); + Navigation.closeRHPFlow(); + }, [duplicateWorkspace?.file, duplicateWorkspace?.name, duplicateWorkspace?.policyID, policy, selectedItems, translate]); const updateSelectedItems = useCallback( (listItem: ListItem) => { @@ -220,8 +264,10 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm ); useEffect(() => { - getWorkspaceMembers(); - }, [getWorkspaceMembers]); + fetchWorkspaceRelatedData(); + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); return ( <> diff --git a/src/types/onyx/DuplicateWorkspace.ts b/src/types/onyx/DuplicateWorkspace.ts index 453cdfb441a59..d3a0c05e72b39 100644 --- a/src/types/onyx/DuplicateWorkspace.ts +++ b/src/types/onyx/DuplicateWorkspace.ts @@ -1,4 +1,3 @@ -import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import type * as OnyxCommon from './OnyxCommon'; /** Model of plaid data */ @@ -10,7 +9,7 @@ type DuplicateWorkspace = { name?: string; /** Workspace avatar */ - file?: File | CustomRNImageManipulatorResult; + file?: File; /** Whether the data is being fetched from server */ isLoading?: boolean; From 401e21b57a426a7e3c9f001c89b3541e22299a1d Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 5 Aug 2025 11:42:35 +0300 Subject: [PATCH 10/29] rollback pod.lock --- ios/Podfile.lock | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 121f790fd7b0b..e401c731e4aff 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2873,7 +2873,7 @@ PODS: - RNGoogleSignin (10.0.1): - GoogleSignIn (~> 7.0) - React-Core - - RNLiveMarkdown (0.1.298): + - RNLiveMarkdown (0.1.294): - DoubleConversion - glog - hermes-engine @@ -3003,7 +3003,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated (3.19.0): + - RNReanimated (3.17.1): - DoubleConversion - glog - hermes-engine @@ -3026,10 +3026,10 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.19.0) - - RNReanimated/worklets (= 3.19.0) + - RNReanimated/reanimated (= 3.17.1) + - RNReanimated/worklets (= 3.17.1) - Yoga - - RNReanimated/reanimated (3.19.0): + - RNReanimated/reanimated (3.17.1): - DoubleConversion - glog - hermes-engine @@ -3052,9 +3052,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.19.0) + - RNReanimated/reanimated/apple (= 3.17.1) - Yoga - - RNReanimated/reanimated/apple (3.19.0): + - RNReanimated/reanimated/apple (3.17.1): - DoubleConversion - glog - hermes-engine @@ -3078,7 +3078,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNReanimated/worklets (3.19.0): + - RNReanimated/worklets (3.17.1): - DoubleConversion - glog - hermes-engine @@ -3101,9 +3101,9 @@ PODS: - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/worklets/apple (= 3.19.0) + - RNReanimated/worklets/apple (= 3.17.1) - Yoga - - RNReanimated/worklets/apple (3.19.0): + - RNReanimated/worklets/apple (3.17.1): - DoubleConversion - glog - hermes-engine @@ -3780,7 +3780,7 @@ SPEC CHECKSUMS: ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c FullStory: 3bf0840584001b8fba01e90f9f660db43025521d fullstory_react-native: eea992f5f335d6b73cb5c3cc06942660388ef92f - glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 + glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 GoogleAppMeasurement: 0dfca1a4b534d123de3945e28f77869d10d0d600 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db @@ -3849,7 +3849,7 @@ SPEC CHECKSUMS: react-native-geolocation: d5114c4ea019574f76344eca8503555ba1208b46 react-native-image-picker: 05b58a33b780623da2f891c55dcc57c6aa867d83 react-native-key-command: b04997038f8fe4a03b711abe36cd84ced93c50b3 - react-native-keyboard-controller: bf564ee7ea1a9b4681aa83989a06f3990b4291a1 + react-native-keyboard-controller: 2602bca1715435e3742c5a69d45ff42c1d97e1b9 react-native-launch-arguments: 5f41e0abf88a15e3c5309b8875d6fd5ac43df49d react-native-netinfo: b8577cba87fbe7b7740c279ee23d25d7c5d87666 react-native-pager-view: 12c1b7b6f50efb3e5d57d62ea5c601cba0585bfd @@ -3905,13 +3905,13 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: b249b5be5a3659025aed8898aaaafd567dc2d660 RNGoogleSignin: ccaa4a81582cf713eea562c5dd9dc1961a715fd0 - RNLiveMarkdown: 737ec283c8c639885884a04ed6836c2878e14a19 + RNLiveMarkdown: a2f598d3a4a025543185c2b5aa6ab86e9dfaaf8c RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 rnmapbox-maps: a5569b64e3ca5fcd1be1f9d5bb478bf601d2a033 RNNitroSQLite: a1b0f1a665c54fc0f2510a292c1bf8ac4d44231b RNPermissions: fd6b2676e74ecb6d2dec0a6168502ab7af733e34 RNReactNativeHapticFeedback: 85c0a6ff490d52f5e8073040296fefe5945ebbfa - RNReanimated: 5794e936ec892ebd4a0143cbcd5ff0e9ecb1573d + RNReanimated: 71c3cc66ee4866e550ddc202e01cd670de3b9ed5 RNScreens: d9d5d8a2a484bb4446968bfa00db991f1117db44 RNShare: 1e3e15a3d2608acde2808bc35448e2344e38e15b RNSound: 6c156f925295bdc83e8e422e7d8b38d33bc71852 @@ -3928,4 +3928,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: f7a8d931f2dd2be0fed9bb22df37140219a07ff3 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 From 8c7834bf6032aa0de60141c6c149adc28bca1808 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 5 Aug 2025 14:21:09 +0300 Subject: [PATCH 11/29] fix checks --- src/libs/API/types.ts | 1 + src/libs/actions/Policy/Policy.ts | 5 +++-- src/types/onyx/DuplicateWorkspace.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f48d31519942f..a1f864fa2cb5a 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -824,6 +824,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams; [WRITE_COMMANDS.PAY_INVOICE]: Parameters.PayInvoiceParams; [WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams; + [WRITE_COMMANDS.DUPLICATE_POLICY]: Parameters.DuplicateWorkspaceParams; [WRITE_COMMANDS.MERGE_DUPLICATES]: Parameters.MergeDuplicatesParams; [WRITE_COMMANDS.RESOLVE_DUPLICATES]: Parameters.ResolveDuplicatesParams; [WRITE_COMMANDS.UPDATE_SUBSCRIPTION_TYPE]: Parameters.UpdateSubscriptionTypeParams; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index bc345ac7d4358..4e1cafe0c9e35 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -63,6 +63,7 @@ import type { import type UpdatePolicyMembersCustomFieldsParams from '@libs/API/parameters/UpdatePolicyMembersCustomFieldsParams'; import type {ApiRequestCommandParameters} from '@libs/API/types'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -165,7 +166,7 @@ type DuplicatePolicyDataOptions = { policyID?: string; welcomeNote: string; parts: Record; - file?: File; + file?: File | CustomRNImageManipulatorResult; }; const allPolicies: OnyxCollection = {}; @@ -2700,7 +2701,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp } // We need to clone the file to prevent non-indexable errors. - const clonedFile = file ? createFile(file) : undefined; + const clonedFile = file ? createFile(file as File) : undefined; const params: DuplicateWorkspaceParams = { policyID: policy.id, diff --git a/src/types/onyx/DuplicateWorkspace.ts b/src/types/onyx/DuplicateWorkspace.ts index d3a0c05e72b39..453cdfb441a59 100644 --- a/src/types/onyx/DuplicateWorkspace.ts +++ b/src/types/onyx/DuplicateWorkspace.ts @@ -1,3 +1,4 @@ +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import type * as OnyxCommon from './OnyxCommon'; /** Model of plaid data */ @@ -9,7 +10,7 @@ type DuplicateWorkspace = { name?: string; /** Workspace avatar */ - file?: File; + file?: File | CustomRNImageManipulatorResult; /** Whether the data is being fetched from server */ isLoading?: boolean; From 7605427f2a6ecacf0689db4bc25d3e28ded0a03e Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 6 Aug 2025 10:53:09 +0300 Subject: [PATCH 12/29] add logic to auto scroll and highlight --- src/libs/actions/Policy/Policy.ts | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 42 ++++++++++++++++--- .../duplicate/WorkspaceDuplicatePage.tsx | 6 +-- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 4e1cafe0c9e35..46fbbd81ccfe0 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2704,7 +2704,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp const clonedFile = file ? createFile(file as File) : undefined; const params: DuplicateWorkspaceParams = { - policyID: policy.id, + policyID, adminsChatReportID, expenseChatReportID, policyName, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 706a007bc07d2..8b7d24d29dd64 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -1,6 +1,6 @@ -import {useRoute} from '@react-navigation/native'; -import React, {useCallback, useMemo, useState} from 'react'; -import {FlatList, View} from 'react-native'; +import {useIsFocused, useRoute} from '@react-navigation/native'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {FlatList, InteractionManager, View} from 'react-native'; import type {ValueOf} from 'type-fest'; import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; @@ -35,7 +35,16 @@ import useSearchResults from '@hooks/useSearchResults'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isConnectionInProgress} from '@libs/actions/connections'; -import {calculateBillNewDot, clearDeleteWorkspaceError, clearErrors, deleteWorkspace, leaveWorkspace, removeWorkspace, updateDefaultPolicy} from '@libs/actions/Policy/Policy'; +import { + calculateBillNewDot, + clearDeleteWorkspaceError, + clearDuplicateWorkspace, + clearErrors, + deleteWorkspace, + leaveWorkspace, + removeWorkspace, + updateDefaultPolicy, +} from '@libs/actions/Policy/Policy'; import {callFunctionIfActionIsAllowed, isSupportAuthToken} from '@libs/actions/Session'; import {filterInactiveCards} from '@libs/CardUtils'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; @@ -104,11 +113,14 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi clearErrors(policyID); } +const onScrollToIndexFailed = () => {}; + function WorkspacesListPage() { const theme = useTheme(); const styles = useThemeStyles(); const {translate, localeCompare} = useLocalize(); const {isOffline} = useNetwork(); + const isFocused = useIsFocused(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const [allConnectionSyncProgresses] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS, {canBeMissing: true}); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); @@ -119,6 +131,7 @@ function WorkspacesListPage() { const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const shouldShowLoadingIndicator = isLoadingApp && !isOffline; const route = useRoute>(); + const [duplicateWorkspace] = useOnyx(ONYXKEYS.DUPLICATE_WORKSPACE, {canBeMissing: false}); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [policyIDToDelete, setPolicyIDToDelete] = useState(); @@ -138,6 +151,7 @@ function WorkspacesListPage() { selector: filterInactiveCards, canBeMissing: true, }); + const flatlistRef = useRef(null); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const policyToDelete = getPolicy(policyIDToDelete); @@ -176,6 +190,8 @@ function WorkspacesListPage() { const isAdmin = isPolicyAdmin(item as unknown as PolicyType, session?.email); const isOwner = item.ownerAccountID === session?.accountID; const isDefault = activePolicyID === item.policyID; + const shouldAnimateInHighlight = duplicateWorkspace?.policyID === item.policyID; + const threeDotsMenuItems: PopoverMenuItem[] = [ { icon: Expensicons.Building, @@ -264,7 +280,7 @@ function WorkspacesListPage() { workspaceIcon={item.icon} ownerAccountID={item.ownerAccountID} workspaceType={item.type} - shouldAnimateInHighlight={item.shouldAnimateInHighlight} + shouldAnimateInHighlight={shouldAnimateInHighlight} isJoinRequestPending={item?.isJoinRequestPending} rowStyles={hovered && styles.hoveredComponentBG} layoutWidth={isLessThanMediumScreen ? CONST.LAYOUT_WIDTH.NARROW : CONST.LAYOUT_WIDTH.WIDE} @@ -285,6 +301,7 @@ function WorkspacesListPage() { styles.mb2, styles.mh5, styles.ph5, + duplicateWorkspace?.policyID, styles.hoveredComponentBG, translate, styles.offlineFeedback.deleted, @@ -375,6 +392,19 @@ function WorkspacesListPage() { const sortWorkspace = useCallback((workspaceItems: WorkspaceItem[]) => workspaceItems.sort((a, b) => localeCompare(a.title, b.title)), [localeCompare]); const [inputValue, setInputValue, filteredWorkspaces] = useSearchResults(workspaces, filterWorkspace, sortWorkspace); + useEffect(() => { + if (isEmptyObject(duplicateWorkspace) || !filteredWorkspaces.length || !isFocused) { + return; + } + const duplicateWorkspaceIndex = filteredWorkspaces.findIndex((workspace) => workspace.policyID === duplicateWorkspace.policyID); + if (duplicateWorkspaceIndex > 0) { + flatlistRef.current?.scrollToIndex({index: duplicateWorkspaceIndex, animated: false}); + InteractionManager.runAfterInteractions(() => { + clearDuplicateWorkspace(); + }); + } + }, [duplicateWorkspace, isFocused, filteredWorkspaces]); + const listHeaderComponent = ( <> {isLessThanMediumScreen && } @@ -490,7 +520,9 @@ function WorkspacesListPage() { {!shouldUseNarrowLayout && {getHeaderButton()}} {shouldUseNarrowLayout && {getHeaderButton()}} >(); const policyID = route?.params?.policyID; - useEffect(() => { - return () => { - clearDuplicateWorkspace(); - }; - }, []); + useEffect(clearDuplicateWorkspace, []); return ( Date: Wed, 6 Aug 2025 16:14:11 +0300 Subject: [PATCH 13/29] add api layer --- .../parameters/DuplicateWorkspaceParams.ts | 1 + src/libs/API/types.ts | 2 +- src/libs/actions/Policy/Policy.ts | 22 ++++++++++--------- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 7 +++--- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/libs/API/parameters/DuplicateWorkspaceParams.ts b/src/libs/API/parameters/DuplicateWorkspaceParams.ts index 13aa985221dd7..77b396fba144d 100644 --- a/src/libs/API/parameters/DuplicateWorkspaceParams.ts +++ b/src/libs/API/parameters/DuplicateWorkspaceParams.ts @@ -2,6 +2,7 @@ import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; type DuplicateWorkspaceParams = { policyID: string; + targetPolicyID: string; policyName: string; parts: Record; announceChatReportID: string; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a1f864fa2cb5a..0fab6063974ef 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -144,7 +144,7 @@ const WRITE_COMMANDS = { UPDATE_WORKSPACE_DESCRIPTION: 'UpdateWorkspaceDescription', UPDATE_WORKSPACE_MEMBERS_ROLE: 'UpdateWorkspaceMembersRole', CREATE_WORKSPACE: 'CreateWorkspace', - DUPLICATE_POLICY: 'Policy_Copy', + DUPLICATE_POLICY: 'DuplicatePolicy', CREATE_WORKSPACE_FROM_IOU_PAYMENT: 'CreateWorkspaceFromIOUPayment', UPDATE_POLICY_MEMBERS_CUSTOM_FIELDS: 'UpdatePolicyMembersCustomFields', SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 46fbbd81ccfe0..3d792c4a80a44 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -164,6 +164,7 @@ type BuildPolicyDataOptions = { type DuplicatePolicyDataOptions = { policyName: string; policyID?: string; + targetPolicyID?: string; welcomeNote: string; parts: Record; file?: File | CustomRNImageManipulatorResult; @@ -2476,8 +2477,7 @@ function createDraftWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policy } function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOptions) { - const {policyName = '', policyID = generatePolicyID(), file, welcomeNote, parts} = options; - const {customUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(policy?.outputCurrency); + const {policyName = '', policyID = generatePolicyID(), file, welcomeNote, parts, targetPolicyID = generatePolicyID()} = options; const { adminsChatReportID, @@ -2489,22 +2489,23 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp expenseReportActionData, expenseCreatedReportActionID, pendingChatMembers, - } = ReportUtils.buildOptimisticWorkspaceChats(policyID, policyName); + } = ReportUtils.buildOptimisticWorkspaceChats(targetPolicyID, policyName); const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + const {customUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(policy?.outputCurrency); - const optimisticAnnounceChat = ReportUtils.buildOptimisticAnnounceChat(policyID, [...policyMemberAccountIDs]); + const optimisticAnnounceChat = ReportUtils.buildOptimisticAnnounceChat(targetPolicyID, [...policyMemberAccountIDs]); const announceRoomChat = optimisticAnnounceChat.announceChatData; - const optimisticCategoriesData = buildOptimisticPolicyCategories(policyID, Object.values(CONST.POLICY.DEFAULT_CATEGORIES)); + const optimisticCategoriesData = buildOptimisticPolicyCategories(targetPolicyID, Object.values(CONST.POLICY.DEFAULT_CATEGORIES)); // WARNING: The data below should be kept in sync with the API so we create the policy with the correct configuration. const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${targetPolicyID}`, value: { ...policy, - id: policyID, + id: targetPolicyID, name: policyName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, pendingFields: { @@ -2568,7 +2569,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp }, { onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${targetPolicyID}`, value: null, }, { @@ -2587,7 +2588,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp const successData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${targetPolicyID}`, value: { pendingAction: null, pendingFields: { @@ -2662,7 +2663,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp const failureData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + key: `${ONYXKEYS.COLLECTION.POLICY}${targetPolicyID}`, value: {employeeList: null}, }, { @@ -2705,6 +2706,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp const params: DuplicateWorkspaceParams = { policyID, + targetPolicyID, adminsChatReportID, expenseChatReportID, policyName, diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 161c80a0b92c1..d1f4e7e1a47e0 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -211,7 +211,8 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm } duplicateWorkspaceAction(policy, { policyName: duplicateWorkspace.name, - policyID: duplicateWorkspace.policyID, + policyID, + targetPolicyID: duplicateWorkspace.policyID, welcomeNote: `${translate('workspace.duplicateWorkspace.welcomeNote')} ${duplicateWorkspace.name}`, parts: { people: selectedItems.includes('members'), @@ -221,7 +222,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm tags: selectedItems.includes('tags'), taxes: selectedItems.includes('taxes'), reimbursements: selectedItems.includes('invoices'), - expenses: selectedItems.includes('reports'), + expenses: selectedItems.includes('rules'), customUnits: selectedItems.includes('distanceRates'), invoices: selectedItems.includes('invoices'), exportLayouts: selectedItems.includes('workflows'), @@ -229,7 +230,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm file: duplicateWorkspace?.file, }); Navigation.closeRHPFlow(); - }, [duplicateWorkspace?.file, duplicateWorkspace?.name, duplicateWorkspace?.policyID, policy, selectedItems, translate]); + }, [duplicateWorkspace?.file, duplicateWorkspace.name, duplicateWorkspace.policyID, policy, policyID, selectedItems, translate]); const updateSelectedItems = useCallback( (listItem: ListItem) => { From 92c8b71bb6160efdcdeb91273f37a7a8b5244897 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 6 Aug 2025 16:26:56 +0300 Subject: [PATCH 14/29] fix ts --- .../duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index d1f4e7e1a47e0..6b4896157cc42 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -230,7 +230,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm file: duplicateWorkspace?.file, }); Navigation.closeRHPFlow(); - }, [duplicateWorkspace?.file, duplicateWorkspace.name, duplicateWorkspace.policyID, policy, policyID, selectedItems, translate]); + }, [duplicateWorkspace?.file, duplicateWorkspace?.name, duplicateWorkspace?.policyID, policy, policyID, selectedItems, translate]); const updateSelectedItems = useCallback( (listItem: ListItem) => { From 66fd6a78d8c10446fb50b3b6aa4c66976f988fa0 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 7 Aug 2025 16:43:09 +0300 Subject: [PATCH 15/29] add correct scroll and translations --- src/languages/de.ts | 18 +++++++------- src/languages/es.ts | 22 ++++++++--------- src/languages/fr.ts | 18 +++++++------- src/languages/it.ts | 22 ++++++++--------- src/languages/ja.ts | 22 ++++++++--------- src/languages/nl.ts | 22 ++++++++--------- src/languages/pl.ts | 22 ++++++++--------- src/languages/pt-BR.ts | 22 ++++++++--------- src/languages/zh-hans.ts | 24 +++++++++---------- .../parameters/DuplicateWorkspaceParams.ts | 2 +- src/libs/actions/Policy/Policy.ts | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 9 ++++--- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 2 +- 13 files changed, 105 insertions(+), 102 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 28f4fb3e3e718..218c97f57863d 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4799,16 +4799,16 @@ const translations = { updateTaxCodeFailureMessage: 'Beim Aktualisieren des Steuercodes ist ein Fehler aufgetreten, bitte versuchen Sie es erneut.', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Benennen Sie Ihren neuen Arbeitsbereich', + selectFeatures: 'Auswählen der zu kopierenden Features', + whichFeatures: 'Welche Funktionen möchten Sie in Ihren neuen Arbeitsbereich kopieren?', + confirmDuplicate: 'Möchten Sie wirklich fortfahren?', + categories: 'Kategorien und Ihre Auto-Kategorisierungsregeln', + reimbursementAccount: 'Erstattungskonto', + delayedSubmission: 'verspätete Einreichung', + welcomeNote: 'Bitte beginnen Sie mit der Nutzung meines neuen Arbeitsbereichs', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Sie sind dabei, ${newWorkspaceName ?? ''} mit den ${totalMembers ?? 0} Personen zu teilen, die ${oldWorkspaceName ?? ''} mit ihnen geteilt haben. Bitte beachten Sie, dass jede dieser Personen eine Benachrichtigung erhält, dass sie sich nun im ${newWorkspaceName ?? ''} befindet.`, }, emptyWorkspace: { title: 'Erstellen Sie einen Arbeitsbereich', diff --git a/src/languages/es.ts b/src/languages/es.ts index 816b703232188..d18dd7063686a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3400,8 +3400,8 @@ const translations = { memberNotFound: 'Miembro no encontrado. Para invitar a un nuevo miembro al espacio de trabajo, por favor, utiliza el botón invitar que está arriba.', notAuthorized: `No tienes acceso a esta página. Si estás intentando unirte a este espacio de trabajo, pide al dueño del espacio de trabajo que te añada como miembro. ¿Necesitas algo más? Comunícate con ${CONST.EMAIL.CONCIERGE}`, goToWorkspace: 'Ir al espacio de trabajo', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'Duplicar espacio de trabajo', + duplicateWorkspacePrefix: 'Duplicar', goToWorkspaces: 'Ir a espacios de trabajo', clearFilter: 'Borrar filtro', workspaceName: 'Nombre del espacio de trabajo', @@ -4792,16 +4792,16 @@ const translations = { updateTaxCodeFailureMessage: 'Se produjo un error al actualizar el código tributario, inténtelo nuevamente', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Nombra tu nuevo espacio de trabajo', + selectFeatures: 'Selecciona las funciones a copiar', + whichFeatures: '¿Qué funciones deseas copiar a tu nuevo espacio de trabajo?', + confirmDuplicate: '¿Estás seguro de que deseas continuar?', + categories: 'categorías y tus reglas de auto-categorización', + reimbursementAccount: 'cuenta de reembolso', + delayedSubmission: 'presentación retrasada', + welcomeNote: 'Por favor, comience a utilizar mi nuevo espacio de trabajo.', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Estás a punto de compartir ${newWorkspaceName ?? ''} con las ${totalMembers ?? 0} personas que ya tienen acceso a ${oldWorkspaceName ?? ''}. Ten en cuenta que cada una de estas personas recibirá una notificación informándoles que ahora forman parte de ${newWorkspaceName ?? ''}.`, }, emptyWorkspace: { title: 'Crea un espacio de trabajo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3dd9fee0a4872..6548acff0255b 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4815,16 +4815,16 @@ const translations = { updateTaxCodeFailureMessage: "Une erreur s'est produite lors de la mise à jour du code fiscal, veuillez réessayer.", }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Nombra tu nuevo espacio de trabajo', + selectFeatures: 'Selecciona las funciones que quieres copiar', + whichFeatures: '¿Qué funciones quieres copiar a tu nuevo espacio de trabajo?', + confirmDuplicate: '¿Estás seguro de que quieres continuar?', + categories: 'Categorías y tus reglas de categorización automática', + reimbursementAccount: 'Cuenta de reembolso', + delayedSubmission: 'Envío retrasado', + welcomeNote: 'Empieza a usar mi nuevo espacio de trabajo', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Estás a punto de compartir ${newWorkspaceName ?? ''} con las ${totalMembers ?? 0} personas que han compartido ${oldWorkspaceName ?? ''} con ellas. Tenga en cuenta que cada una de estas personas recibirá una notificación informándoles que ahora están en ${newWorkspaceName ?? ''}.`, }, emptyWorkspace: { title: 'Créer un espace de travail', diff --git a/src/languages/it.ts b/src/languages/it.ts index e1e0720c6feb9..ac7879dbe6eb6 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3429,9 +3429,9 @@ const translations = { memberNotFound: 'Membro non trovato. Per invitare un nuovo membro al workspace, utilizza il pulsante di invito sopra.', notAuthorized: `Non hai accesso a questa pagina. Se stai cercando di unirti a questo spazio di lavoro, chiedi semplicemente al proprietario dello spazio di lavoro di aggiungerti come membro. Qualcos'altro? Contatta ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Vai allo spazio di lavoro', - duplicateWorkspace: 'Duplicate Workspace', + duplicateWorkspace: 'Area di lavoro duplicata', duplicateWorkspacePrefix: 'Duplicate', - goToWorkspaces: 'Vai agli spazi di lavoro', + goToWorkspaces: 'Duplicato', clearFilter: 'Cancella filtro', workspaceName: 'Nome del workspace', workspaceOwner: 'Proprietario', @@ -4814,16 +4814,16 @@ const translations = { updateTaxCodeFailureMessage: "Si è verificato un errore durante l'aggiornamento del codice fiscale, riprova.", }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Assegna un nome al tuo nuovo spazio di lavoro', + selectFeatures: 'Seleziona le funzionalità da copiare', + whichFeatures: 'Quali funzionalità vuoi copiare nel tuo nuovo spazio di lavoro?', + confirmDuplicate: 'Sei sicuro di voler procedere?', + categories: 'Categorie e regole di categorizzazione automatica', + reimbursementAccount: 'Account di rimborso', + delayedSubmission: 'Invio ritardato', + welcomeNote: 'Inizia a utilizzare il mio nuovo spazio di lavoro', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Stai per condividere ${newWorkspaceName ?? ''} questo con le ${totalMembers ?? 0} persone con cui hai condiviso ${oldWorkspaceName ?? ''}. Tieni presente che ciascuna di queste persone riceverà una notifica che la informerà che ora si trova su ${newWorkspaceName ?? ''}.`, }, emptyWorkspace: { title: "Crea un'area di lavoro", diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 12c1baad4173b..a2ab96c61f960 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3429,8 +3429,8 @@ const translations = { memberNotFound: 'メンバーが見つかりません。新しいメンバーをワークスペースに招待するには、上の招待ボタンを使用してください。', notAuthorized: `このページにアクセスする権限がありません。このワークスペースに参加しようとしている場合は、ワークスペースのオーナーにメンバーとして追加してもらってください。他に何かお困りですか?${CONST.EMAIL.CONCIERGE}にお問い合わせください。`, goToWorkspace: 'ワークスペースに移動', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'ワークスペースの複製', + duplicateWorkspacePrefix: '重複', goToWorkspaces: 'ワークスペースに移動', clearFilter: 'フィルターをクリア', workspaceName: 'ワークスペース名', @@ -4793,16 +4793,16 @@ const translations = { updateTaxCodeFailureMessage: '税コードの更新中にエラーが発生しました。もう一度お試しください。', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: '新しいワークスペースに名前を付けてください', + selectFeatures: 'コピーする機能を選択してください', + whichFeatures: '新しいワークスペースにコピーする機能はどれですか?', + confirmDuplicate: '続行してもよろしいですか?', + categories: 'カテゴリと自動分類ルール', + reimbursementAccount: '払い戻し口座', + delayedSubmission: '遅延送信', + welcomeNote: '新しいワークスペースの使用を開始してください', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `${newWorkspaceName ?? ''} を、${oldWorkspaceName ?? ''} を共有している ${totalMembers ?? 0} 人と共有しようとしています。これらの各ユーザーには、現在 ${newWorkspaceName ?? ''}. にいることを知らせる通知が届きますのでご注意ください`, }, emptyWorkspace: { title: 'ワークスペースを作成', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index c245b60912700..1e6774e362b84 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3436,8 +3436,8 @@ const translations = { memberNotFound: 'Lid niet gevonden. Om een nieuw lid aan de werkruimte toe te voegen, gebruik de uitnodigingsknop hierboven.', notAuthorized: `Je hebt geen toegang tot deze pagina. Als je probeert lid te worden van deze werkruimte, vraag dan de eigenaar van de werkruimte om je als lid toe te voegen. Iets anders? Neem contact op met ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Ga naar werkruimte', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'Dubbele werkruimte', + duplicateWorkspacePrefix: 'Duplicaat', goToWorkspaces: 'Ga naar werkruimtes', clearFilter: 'Filter wissen', workspaceName: 'Werkruimte naam', @@ -4816,16 +4816,16 @@ const translations = { updateTaxCodeFailureMessage: 'Er is een fout opgetreden bij het bijwerken van de belastingcode, probeer het opnieuw.', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Geef je nieuwe werkruimte een naam', + selectFeatures: 'Selecteer te kopiëren functies', + whichFeatures: 'Welke functies wil je kopiëren naar je nieuwe werkruimte?', + confirmDuplicate: 'Weet je zeker dat je wilt doorgaan?', + categories: 'categorieën en je regels voor automatische categorisatie', + reimbursementAccount: 'vergoedingsrekening', + delayedSubmission: 'vertraagde indiening', + welcomeNote: 'Ga aan de slag met mijn nieuwe werkruimte', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Je staat op het punt om ${newWorkspaceName ?? ''} dit te delen met de ${totalMembers ?? 0} mensen met wie ${oldWorkspaceName ?? ''} gedeeld is. Houd er rekening mee dat elk van deze personen een melding ontvangt met de mededeling dat ze nu op ${newWorkspaceName ?? ''}.`, }, emptyWorkspace: { title: 'Maak een werkruimte aan', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 0f0d2facc767c..b6a3cbcf31db1 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3429,8 +3429,8 @@ const translations = { memberNotFound: 'Nie znaleziono członka. Aby zaprosić nowego członka do przestrzeni roboczej, użyj przycisku zaproszenia powyżej.', notAuthorized: `Nie masz dostępu do tej strony. Jeśli próbujesz dołączyć do tego miejsca pracy, poproś właściciela miejsca pracy o dodanie Cię jako członka. Coś innego? Skontaktuj się z ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Przejdź do przestrzeni roboczej', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'Duplikat obszaru roboczego', + duplicateWorkspacePrefix: 'Duplikat', goToWorkspaces: 'Przejdź do przestrzeni roboczych', clearFilter: 'Wyczyść filtr', workspaceName: 'Nazwa przestrzeni roboczej', @@ -4805,16 +4805,16 @@ const translations = { updateTaxCodeFailureMessage: 'Wystąpił błąd podczas aktualizacji kodu podatkowego, spróbuj ponownie.', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Nazwij swój nowy obszar roboczy', + selectFeatures: 'Wybierz funkcje do skopiowania', + whichFeatures: 'Które funkcje chcesz skopiować do nowego obszaru roboczego?', + confirmDuplicate: 'Czy na pewno chcesz kontynuować?', + categoris: 'kategorie i zasady automatycznej kategoryzacji', + rewardmentAccount: 'konto zwrotu', + expectedSubmission: 'opóźnione przesłanie', + welcomeNote: 'Proszę rozpocząć korzystanie z mojego nowego obszaru roboczego', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Zamierzasz udostępnić ${newWorkspaceName ?? ''} to ${totalMembers ?? 0} osobom, które mają ${oldWorkspaceName ?? ''} udostępniono im. Pamiętaj, że każda z tych osób otrzyma powiadomienie informujące, że znajduje się teraz w ${newWorkspaceName ?? ''}.`, }, emptyWorkspace: { title: 'Utwórz przestrzeń roboczą', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 88ae985ab7a32..995794cd809fe 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3434,8 +3434,8 @@ const translations = { memberNotFound: 'Membro não encontrado. Para convidar um novo membro para o espaço de trabalho, por favor, use o botão de convite acima.', notAuthorized: `Você não tem acesso a esta página. Se você está tentando entrar neste espaço de trabalho, basta pedir ao proprietário do espaço de trabalho para adicioná-lo como membro. Algo mais? Entre em contato com ${CONST.EMAIL.CONCIERGE}.`, goToWorkspace: 'Ir para o espaço de trabalho', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: 'Espaço de trabalho duplicado', + duplicateWorkspacePrefix: 'Duplicado', goToWorkspaces: 'Ir para espaços de trabalho', clearFilter: 'Limpar filtro', workspaceName: 'Nome do espaço de trabalho', @@ -4811,16 +4811,16 @@ const translations = { updateTaxCodeFailureMessage: 'Ocorreu um erro ao atualizar o código de imposto, por favor, tente novamente.', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', + title: 'Nomeie seu novo espaço de trabalho', + selectFeatures: 'Selecione os recursos a serem copiados', + whichFeatures: 'Quais recursos você deseja copiar para o seu novo espaço de trabalho?', + confirmDuplicate: 'Tem certeza de que deseja prosseguir?', + categories: 'categorias e suas regras de categorização automática', + reimbursementAccount: 'conta de reembolso', + delayedSubmission: 'envio atrasado', + welcomeNote: 'Comece a usar meu novo espaço de trabalho', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + `Você está prestes a compartilhar ${newWorkspaceName ?? ''} com as ${totalMembers ?? 0} pessoas que compartilharam ${oldWorkspaceName ?? ''} com elas. Esteja ciente de que cada uma dessas pessoas receberá uma notificação informando que agora estão no ${newWorkspaceName ?? ''}.`, }, emptyWorkspace: { title: 'Criar um espaço de trabalho', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 257a263ca4321..e4a193bdb19af 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3389,8 +3389,8 @@ const translations = { memberNotFound: '未找到成员。要邀请新成员加入工作区,请使用上面的邀请按钮。', notAuthorized: `您无权访问此页面。如果您正在尝试加入此工作区,请请求工作区所有者将您添加为成员。还有其他问题?请联系${CONST.EMAIL.CONCIERGE}。`, goToWorkspace: '前往工作区', - duplicateWorkspace: 'Duplicate Workspace', - duplicateWorkspacePrefix: 'Duplicate', + duplicateWorkspace: '重复工作区', + duplicateWorkspacePrefix: '复制', goToWorkspaces: '前往工作区', clearFilter: '清除筛选器', workspaceName: '工作区名称', @@ -4730,16 +4730,16 @@ const translations = { updateTaxCodeFailureMessage: '更新税码时发生错误,请重试', }, duplicateWorkspace: { - title: 'Name your new workspace', - selectFeatures: 'Select features to copy', - whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', - categories: 'categories and your auto-categorization rules', - reimbursementAccount: 'reimbursement account', - delayedSubmission: 'delayed submission', - welcomeNote: 'Please start using my new workspace', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + title: '命名您的新工作区', + selectFeatures: '选择要复制的功能', + whichFeatures: '您想要将哪些功能复制到您的新工作区?', + confirmedDuplicate: '您确定要继续吗?', + categories: '类别和您的自动分类规则', + reimbursementAccount: '报销账户', + delayedSubmission: '延迟提交', + welcomeNote: '请开始使用我的新工作区', + confirmedTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + `您即将与已共享 ${oldWorkspaceName ?? ''} 的 ${totalMembers ?? 0} 位用户共享此 ${newWorkspaceName ?? ''}。请注意,这些人都会收到一条通知,告知他们现在位于 ${newWorkspaceName ?? ''}。`, }, emptyWorkspace: { title: '创建一个工作区', diff --git a/src/libs/API/parameters/DuplicateWorkspaceParams.ts b/src/libs/API/parameters/DuplicateWorkspaceParams.ts index 77b396fba144d..8dc8966550d5a 100644 --- a/src/libs/API/parameters/DuplicateWorkspaceParams.ts +++ b/src/libs/API/parameters/DuplicateWorkspaceParams.ts @@ -4,7 +4,7 @@ type DuplicateWorkspaceParams = { policyID: string; targetPolicyID: string; policyName: string; - parts: Record; + parts: string; announceChatReportID: string; adminsChatReportID: string; welcomeNote: string; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 80801cb622a1e..e26894b2679ab 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2720,7 +2720,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp announceChatReportID: optimisticAnnounceChat.announceChatReportID, announceChatReportActionID: optimisticAnnounceChat.announceChatReportActionID, customUnitID, - parts, + parts: JSON.stringify(parts), welcomeNote, customUnitRateID, file: clonedFile, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 8b7d24d29dd64..11b379ff93ff2 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -113,8 +113,6 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi clearErrors(policyID); } -const onScrollToIndexFailed = () => {}; - function WorkspacesListPage() { const theme = useTheme(); const styles = useThemeStyles(); @@ -522,7 +520,12 @@ function WorkspacesListPage() { { + flatlistRef.current?.scrollToOffset({ + offset: info.averageItemLength * info.index, + animated: true, + }); + }} renderItem={getMenuItem} ListHeaderComponent={listHeaderComponent} keyboardShouldPersistTaps="handled" diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 6b4896157cc42..e5dcb079134c0 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -295,7 +295,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm setIsDuplicateModalOpen(false)} From 08c298d5b0657dfea63a9a0e3994d46193a38669 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 7 Aug 2025 16:48:43 +0300 Subject: [PATCH 16/29] fix ts --- src/languages/pl.ts | 4 ++-- src/languages/zh-hans.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b6a3cbcf31db1..74d101484725e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4809,8 +4809,8 @@ const translations = { selectFeatures: 'Wybierz funkcje do skopiowania', whichFeatures: 'Które funkcje chcesz skopiować do nowego obszaru roboczego?', confirmDuplicate: 'Czy na pewno chcesz kontynuować?', - categoris: 'kategorie i zasady automatycznej kategoryzacji', - rewardmentAccount: 'konto zwrotu', + categories: 'kategorie i zasady automatycznej kategoryzacji', + reimbursementAccount: 'konto zwrotu', expectedSubmission: 'opóźnione przesłanie', welcomeNote: 'Proszę rozpocząć korzystanie z mojego nowego obszaru roboczego', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index e4a193bdb19af..fa1665af09cc6 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4738,7 +4738,7 @@ const translations = { reimbursementAccount: '报销账户', delayedSubmission: '延迟提交', welcomeNote: '请开始使用我的新工作区', - confirmedTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => + confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `您即将与已共享 ${oldWorkspaceName ?? ''} 的 ${totalMembers ?? 0} 位用户共享此 ${newWorkspaceName ?? ''}。请注意,这些人都会收到一条通知,告知他们现在位于 ${newWorkspaceName ?? ''}。`, }, emptyWorkspace: { From 7a94f5d8f38814949a4586708d8cd7ebcc716778 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 7 Aug 2025 17:09:31 +0300 Subject: [PATCH 17/29] fix ts --- src/languages/pl.ts | 2 +- src/languages/zh-hans.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 74d101484725e..e4c3715afdef2 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4811,7 +4811,7 @@ const translations = { confirmDuplicate: 'Czy na pewno chcesz kontynuować?', categories: 'kategorie i zasady automatycznej kategoryzacji', reimbursementAccount: 'konto zwrotu', - expectedSubmission: 'opóźnione przesłanie', + delayedSubmission: 'opóźnione przesłanie', welcomeNote: 'Proszę rozpocząć korzystanie z mojego nowego obszaru roboczego', confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => `Zamierzasz udostępnić ${newWorkspaceName ?? ''} to ${totalMembers ?? 0} osobom, które mają ${oldWorkspaceName ?? ''} udostępniono im. Pamiętaj, że każda z tych osób otrzyma powiadomienie informujące, że znajduje się teraz w ${newWorkspaceName ?? ''}.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index fa1665af09cc6..7beae2448f2da 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4733,7 +4733,7 @@ const translations = { title: '命名您的新工作区', selectFeatures: '选择要复制的功能', whichFeatures: '您想要将哪些功能复制到您的新工作区?', - confirmedDuplicate: '您确定要继续吗?', + confirmDuplicate: '您确定要继续吗?', categories: '类别和您的自动分类规则', reimbursementAccount: '报销账户', delayedSubmission: '延迟提交', From 2bf10b7c6118bd5b80870d89de5d85ca05ddea1f Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 8 Aug 2025 12:36:59 +0300 Subject: [PATCH 18/29] fixes after c+ --- .../duplicate/WorkspaceDuplicateForm.tsx | 4 ++-- .../duplicate/WorkspaceDuplicatePage.tsx | 19 +++++++++++++------ .../WorkspaceDuplicateSelectFeaturesForm.tsx | 2 +- .../WorkspaceDuplicateSelectFeaturesPage.tsx | 19 +++++++++++++------ 4 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx index 67b9f22d41652..5135ac34fc146 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -63,9 +63,9 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { const onSubmit = useCallback( ({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { - const newPolicyID = generatePolicyID(); - setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); if (policyID) { + const newPolicyID = generatePolicyID(); + setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); } }, diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx index 4eed9fe7c7914..e9485b5000497 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicatePage.tsx @@ -3,7 +3,9 @@ import React, {useEffect} from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {WorkspaceDuplicateNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {clearDuplicateWorkspace} from '@userActions/Policy/Policy'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import WorkspaceDuplicateForm from './WorkspaceDuplicateForm'; @@ -14,13 +16,18 @@ function WorkspaceDuplicatePage() { useEffect(clearDuplicateWorkspace, []); return ( - - - + + + + ); } diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index e5dcb079134c0..f9d2a5af474f2 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -164,7 +164,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm : undefined, ]; - return result.filter((item): item is NonNullable => item !== undefined); + return result.filter((item): item is NonNullable => !!item); }, [ policy, translate, diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx index ef3b24f52dcc4..0359e4ef47dd4 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesPage.tsx @@ -3,6 +3,8 @@ import React from 'react'; import ScreenWrapper from '@components/ScreenWrapper'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {WorkspaceDuplicateNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import WorkspaceDuplicateSelectFeaturesForm from './WorkspaceDuplicateSelectFeaturesForm'; @@ -11,13 +13,18 @@ function WorkspaceDuplicateSelectFeaturesPage() { const policyID = route?.params?.policyID; return ( - - - + + + + ); } From f5d84af008bf2c165a122c1dcd93d629ea0c645e Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 8 Aug 2025 14:27:56 +0300 Subject: [PATCH 19/29] fix eslint --- src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx index 5135ac34fc146..40d665572acbf 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -63,11 +63,12 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { const onSubmit = useCallback( ({name, avatarFile}: {name?: string; avatarFile?: File | CustomRNImageManipulatorResult}) => { - if (policyID) { - const newPolicyID = generatePolicyID(); - setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); - Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); + if (!policyID) { + return; } + const newPolicyID = generatePolicyID(); + setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); + Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); }, [policyID], ); From 8f9be80e25247e5bfeb285258fb2bfb7e5a86bdc Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Fri, 8 Aug 2025 16:01:58 +0300 Subject: [PATCH 20/29] remove scrollview --- .../duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index f9d2a5af474f2..9d19c0874d54d 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -2,7 +2,6 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScrollView from '@components/ScrollView'; import SelectionList from '@components/SelectionList'; import MultiSelectListItem from '@components/SelectionList/MultiSelectListItem'; import type {ListItem} from '@components/SelectionList/types'; @@ -273,10 +272,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm return ( <> - + <> {translate('workspace.duplicateWorkspace.selectFeatures')} {translate('workspace.duplicateWorkspace.whichFeatures')} @@ -288,12 +284,13 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm ListItem={MultiSelectListItem} onSelectRow={updateSelectedItems} isAlternateTextMultilineSupported + addBottomSafeAreaPadding showConfirmButton confirmButtonText={translate('common.next')} onConfirm={() => setIsDuplicateModalOpen(true)} /> - + Date: Wed, 13 Aug 2025 10:29:42 +0300 Subject: [PATCH 21/29] fix lint --- src/pages/workspace/WorkspacesListRow.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index 5639add7a83e6..201eb01f6cce9 100644 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -27,7 +27,6 @@ import {getUserFriendlyWorkspaceType} from '@libs/PolicyUtils'; import type {AvatarSource} from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type {AnchorPosition} from '@src/styles'; import type IconAsset from '@src/types/utils/IconAsset'; type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { From 04298e68e6a54a95d4e0b3c63ee3723dc2a7ad8a Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 13 Aug 2025 10:35:43 +0300 Subject: [PATCH 22/29] set select all selected by defaul --- .../duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 9d19c0874d54d..7cb6b30778a46 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -263,6 +263,15 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm [items, selectedItems], ); + useEffect(() => { + if (!items.length) { + return; + } + setSelectedItems(items.map((i) => i.value)); + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [items.length]); + useEffect(() => { fetchWorkspaceRelatedData(); // eslint-disable-next-line react-compiler/react-compiler From 076c9ed335c088d405a8e1a54ffab6a8120e2026 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 13 Aug 2025 15:56:40 +0300 Subject: [PATCH 23/29] fix drawer --- src/languages/de.ts | 2 +- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- src/languages/fr.ts | 2 +- src/languages/it.ts | 2 +- src/languages/ja.ts | 2 +- src/languages/nl.ts | 2 +- src/languages/pl.ts | 2 +- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 2 +- .../duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx | 4 ++-- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c3d718a8c5442..509ecef5b1bfa 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4866,7 +4866,7 @@ const translations = { title: 'Benennen Sie Ihren neuen Arbeitsbereich', selectFeatures: 'Auswählen der zu kopierenden Features', whichFeatures: 'Welche Funktionen möchten Sie in Ihren neuen Arbeitsbereich kopieren?', - confirmDuplicate: 'Möchten Sie wirklich fortfahren?', + confirmDuplicate: '\n\nMöchten Sie wirklich fortfahren?', categories: 'Kategorien und Ihre Auto-Kategorisierungsregeln', reimbursementAccount: 'Erstattungskonto', delayedSubmission: 'verspätete Einreichung', diff --git a/src/languages/en.ts b/src/languages/en.ts index 2a232ad36083d..afecf2f936a5b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4849,7 +4849,7 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: 'Are you sure you want to proceed?', + confirmDuplicate: '\n\nAre you sure you want to proceed?', categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', welcomeNote: 'Please start using my new workspace', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0c1c03bcb234d..6180e7bbd0097 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4859,7 +4859,7 @@ const translations = { title: 'Nombra tu nuevo espacio de trabajo', selectFeatures: 'Selecciona las funciones a copiar', whichFeatures: '¿Qué funciones deseas copiar a tu nuevo espacio de trabajo?', - confirmDuplicate: '¿Estás seguro de que deseas continuar?', + confirmDuplicate: '\n\n¿Estás seguro de que deseas continuar?', categories: 'categorías y tus reglas de auto-categorización', reimbursementAccount: 'cuenta de reembolso', delayedSubmission: 'presentación retrasada', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 8bee1bffdae23..b7ef5f067be53 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4882,7 +4882,7 @@ const translations = { title: 'Nombra tu nuevo espacio de trabajo', selectFeatures: 'Selecciona las funciones que quieres copiar', whichFeatures: '¿Qué funciones quieres copiar a tu nuevo espacio de trabajo?', - confirmDuplicate: '¿Estás seguro de que quieres continuar?', + confirmDuplicate: '\n\n¿Estás seguro de que quieres continuar?', categories: 'Categorías y tus reglas de categorización automática', reimbursementAccount: 'Cuenta de reembolso', delayedSubmission: 'Envío retrasado', diff --git a/src/languages/it.ts b/src/languages/it.ts index d743809746705..e477007dbdc3c 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4881,7 +4881,7 @@ const translations = { title: 'Assegna un nome al tuo nuovo spazio di lavoro', selectFeatures: 'Seleziona le funzionalità da copiare', whichFeatures: 'Quali funzionalità vuoi copiare nel tuo nuovo spazio di lavoro?', - confirmDuplicate: 'Sei sicuro di voler procedere?', + confirmDuplicate: '\n\nSei sicuro di voler procedere?', categories: 'Categorie e regole di categorizzazione automatica', reimbursementAccount: 'Account di rimborso', delayedSubmission: 'Invio ritardato', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 32cd1ffdb6484..b6d4e6781f361 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4860,7 +4860,7 @@ const translations = { title: '新しいワークスペースに名前を付けてください', selectFeatures: 'コピーする機能を選択してください', whichFeatures: '新しいワークスペースにコピーする機能はどれですか?', - confirmDuplicate: '続行してもよろしいですか?', + confirmDuplicate: '\n\n続行してもよろしいですか?', categories: 'カテゴリと自動分類ルール', reimbursementAccount: '払い戻し口座', delayedSubmission: '遅延送信', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 73d0b72f5d983..b324d232b2396 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4883,7 +4883,7 @@ const translations = { title: 'Geef je nieuwe werkruimte een naam', selectFeatures: 'Selecteer te kopiëren functies', whichFeatures: 'Welke functies wil je kopiëren naar je nieuwe werkruimte?', - confirmDuplicate: 'Weet je zeker dat je wilt doorgaan?', + confirmDuplicate: '\n\nWeet je zeker dat je wilt doorgaan?', categories: 'categorieën en je regels voor automatische categorisatie', reimbursementAccount: 'vergoedingsrekening', delayedSubmission: 'vertraagde indiening', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index c1626ad5e005d..f4f8887d709fa 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4872,7 +4872,7 @@ const translations = { title: 'Nazwij swój nowy obszar roboczy', selectFeatures: 'Wybierz funkcje do skopiowania', whichFeatures: 'Które funkcje chcesz skopiować do nowego obszaru roboczego?', - confirmDuplicate: 'Czy na pewno chcesz kontynuować?', + confirmDuplicate: '\n\nCzy na pewno chcesz kontynuować?', categories: 'kategorie i zasady automatycznej kategoryzacji', reimbursementAccount: 'konto zwrotu', delayedSubmission: 'opóźnione przesłanie', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 1ce1f8e1c4ec2..0319bdf86b09b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4878,7 +4878,7 @@ const translations = { title: 'Nomeie seu novo espaço de trabalho', selectFeatures: 'Selecione os recursos a serem copiados', whichFeatures: 'Quais recursos você deseja copiar para o seu novo espaço de trabalho?', - confirmDuplicate: 'Tem certeza de que deseja prosseguir?', + confirmDuplicate: '\n\nTem certeza de que deseja prosseguir?', categories: 'categorias e suas regras de categorização automática', reimbursementAccount: 'conta de reembolso', delayedSubmission: 'envio atrasado', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index fceee4070fc58..ddc71648b4a7c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4796,7 +4796,7 @@ const translations = { title: '命名您的新工作区', selectFeatures: '选择要复制的功能', whichFeatures: '您想要将哪些功能复制到您的新工作区?', - confirmDuplicate: '您确定要继续吗?', + confirmDuplicate: '\n\n您确定要继续吗?', categories: '类别和您的自动分类规则', reimbursementAccount: '报销账户', delayedSubmission: '延迟提交', diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 7cb6b30778a46..292bd9aabbd40 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -306,7 +306,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm onConfirm={confirmDuplicateAndHideModal} onCancel={() => setIsDuplicateModalOpen(false)} prompt={ - + {translate('workspace.duplicateWorkspace.confirmTitle', { newWorkspaceName: duplicateWorkspace?.name, @@ -315,7 +315,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm })} {translate('workspace.duplicateWorkspace.confirmDuplicate')} - + } confirmText={translate('common.proceed')} cancelText={translate('common.cancel')} From b4ae22080e460b3e31361a54bd30c54ddcf41ae7 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 14 Aug 2025 13:30:43 +0300 Subject: [PATCH 24/29] updates after c+ --- src/ROUTES.ts | 4 +- src/components/WorkspaceConfirmationForm.tsx | 33 +++--------- src/hooks/useWorkspaceConfirmationAvatar.tsx | 27 ++++++++++ src/libs/PolicyUtils.ts | 7 --- src/libs/actions/Policy/Policy.ts | 4 +- src/libs/getFirstAlphaNumericCharacter.ts | 8 +++ src/pages/workspace/WorkspacesListPage.tsx | 2 +- .../duplicate/WorkspaceDuplicateForm.tsx | 34 ++++-------- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 54 +++++++++++++------ src/pages/workspace/duplicate/utils.ts | 10 +++- 10 files changed, 105 insertions(+), 78 deletions(-) create mode 100644 src/hooks/useWorkspaceConfirmationAvatar.tsx create mode 100644 src/libs/getFirstAlphaNumericCharacter.ts diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 94b3587c13541..b9c6343aabf9e 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1857,11 +1857,11 @@ const ROUTES = { }, WORKSPACE_DUPLICATE: { route: 'workspace/:policyID/duplicate', - getRoute: (policyID: string) => `workspace/${policyID}/duplicate` as const, + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/duplicate`, backTo), }, WORKSPACE_DUPLICATE_SELECT_FEATURES: { route: 'workspace/:policyID/duplicate/select-features', - getRoute: (policyID: string) => `workspace/${policyID}/duplicate/select-features` as const, + getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`workspace/${policyID}/duplicate/select-features`, backTo), }, WORKSPACE_RECEIPT_PARTNERS: { route: 'workspaces/:policyID/receipt-partners', diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx index 0140e515da7de..7f08e8728d0ec 100644 --- a/src/components/WorkspaceConfirmationForm.tsx +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -5,9 +5,11 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceConfirmationAvatar from '@hooks/useWorkspaceConfirmationAvatar'; import {generateDefaultWorkspaceName, generatePolicyID} from '@libs/actions/Policy/Policy'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import {addErrorMessage} from '@libs/ErrorUtils'; +import getFirstAlphaNumericCharacter from '@libs/getFirstAlphaNumericCharacter'; import Navigation from '@libs/Navigation/Navigation'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import {isRequiredFulfilled} from '@libs/ValidationUtils'; @@ -15,7 +17,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import INPUT_IDS from '@src/types/form/WorkspaceConfirmationForm'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -import Avatar from './Avatar'; import AvatarWithImagePicker from './AvatarWithImagePicker'; import CurrencyPicker from './CurrencyPicker'; import FormProvider from './Form/FormProvider'; @@ -27,13 +28,6 @@ import ScrollView from './ScrollView'; import Text from './Text'; import TextInput from './TextInput'; -function getFirstAlphaNumericCharacter(str = '') { - return str - .normalize('NFD') - .replace(/[^0-9a-z]/gi, '') - .toUpperCase()[0]; -} - type WorkspaceConfirmationSubmitFunctionParams = { name: string; currency: string; @@ -96,7 +90,6 @@ function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButto const currencyParam = route.params && 'currency' in route.params && !!route.params.currency ? (route.params.currency as string) : undefined; const currencyValue = !!currencyList && currencyParam && currencyParam in currencyList ? currencyParam : userCurrency; const [currency] = useState(currencyValue); - useEffect(() => { Navigation.setParams({currency}); }, [currency]); @@ -110,22 +103,12 @@ function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButto const stashedLocalAvatarImage = workspaceAvatar?.avatarUri ?? undefined; - const DefaultAvatar = useCallback( - () => ( - - ), - [workspaceAvatar?.avatarUri, workspaceNameFirstCharacter, styles.alignSelfCenter, styles.avatarXLarge, policyID], - ); + const DefaultAvatar = useWorkspaceConfirmationAvatar({ + policyID, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing cannot be used if left side can be empty string + source: stashedLocalAvatarImage || getDefaultWorkspaceAvatar(workspaceNameFirstCharacter), + name: workspaceNameFirstCharacter, + }); return ( <> diff --git a/src/hooks/useWorkspaceConfirmationAvatar.tsx b/src/hooks/useWorkspaceConfirmationAvatar.tsx new file mode 100644 index 0000000000000..a2c9b541a1b59 --- /dev/null +++ b/src/hooks/useWorkspaceConfirmationAvatar.tsx @@ -0,0 +1,27 @@ +import React, {useCallback} from 'react'; +import Avatar from '@components/Avatar'; +import * as Expensicons from '@components/Icon/Expensicons'; +import CONST from '@src/CONST'; +import useThemeStyles from './useThemeStyles'; + +function useWorkspaceConfirmationAvatar({policyID, source, name}: {policyID: string; source: string; name: string}) { + const styles = useThemeStyles(); + + return useCallback( + () => ( + + ), + [name, policyID, source, styles.alignSelfCenter, styles.avatarXLarge], + ); +} + +export default useWorkspaceConfirmationAvatar; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index bd0daa79d2d1f..e77a1e1e46cde 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1264,12 +1264,6 @@ function getValidConnectedIntegration(policy: Policy | undefined, accountingInte ); } -function getAllValidConnectedIntegration(policy: Policy | undefined, accountingIntegrations?: ConnectionName[]) { - return (accountingIntegrations ?? Object.values(CONST.POLICY.CONNECTIONS.NAME)).filter( - (integration) => !!policy?.connections?.[integration] && !isAuthenticationError(policy, integration), - ); -} - function hasIntegrationAutoSync(policy: Policy | undefined, connectedIntegration?: ConnectionName) { return (connectedIntegration && policy?.connections?.[connectedIntegration]?.config?.autoSync?.enabled) ?? false; } @@ -1666,7 +1660,6 @@ export { getCountOfRequiredTagLists, getActiveEmployeeWorkspaces, isUserInvitedToWorkspace, - getAllValidConnectedIntegration, getPolicyRole, hasIndependentTags, getLengthOfTag, diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 9be7b94ad5a49..1c7705dbc45fc 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2497,7 +2497,8 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp expenseCreatedReportActionID, pendingChatMembers, } = ReportUtils.buildOptimisticWorkspaceChats(targetPolicyID, policyName); - const policyMemberAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)); + const isMemberOptionSelected = parts?.people; + const policyMemberAccountIDs = isMemberOptionSelected ? Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)) : []; const {customUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(policy?.outputCurrency); const optimisticAnnounceChat = ReportUtils.buildOptimisticAnnounceChat(targetPolicyID, [...policyMemberAccountIDs]); @@ -2512,6 +2513,7 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp key: `${ONYXKEYS.COLLECTION.POLICY}${targetPolicyID}`, value: { ...policy, + employeeList: isMemberOptionSelected ? policy.employeeList : {[policy.owner]: policy?.employeeList?.[policy.owner]}, id: targetPolicyID, name: policyName, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, diff --git a/src/libs/getFirstAlphaNumericCharacter.ts b/src/libs/getFirstAlphaNumericCharacter.ts new file mode 100644 index 0000000000000..94d7f5e43e1fc --- /dev/null +++ b/src/libs/getFirstAlphaNumericCharacter.ts @@ -0,0 +1,8 @@ +function getFirstAlphaNumericCharacter(str = '') { + return str + .normalize('NFD') + .replace(/[^0-9a-z]/gi, '') + .toUpperCase()[0]; +} + +export default getFirstAlphaNumericCharacter; diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d712e3e9ec2c9..f936d4a587fab 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -236,7 +236,7 @@ function WorkspacesListPage() { threeDotsMenuItems.push({ icon: Expensicons.Copy, text: translate('workspace.common.duplicateWorkspace'), - onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE.getRoute(item.policyID)) : undefined), + onSelected: () => (item.policyID ? Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE.getRoute(item.policyID, ROUTES.WORKSPACES_LIST.route)) : undefined), }); } diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx index 40d665572acbf..3b19e8101c1d1 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateForm.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; -import Avatar from '@components/Avatar'; import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -14,9 +13,11 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceConfirmationAvatar from '@hooks/useWorkspaceConfirmationAvatar'; import {generatePolicyID, setDuplicateWorkspaceData} from '@libs/actions/Policy/Policy'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import {addErrorMessage} from '@libs/ErrorUtils'; +import getFirstAlphaNumericCharacter from '@libs/getFirstAlphaNumericCharacter'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import {isRequiredFulfilled} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; @@ -25,13 +26,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/WorkspaceDuplicateForm'; -function getFirstAlphaNumericCharacter(str = '') { - return str - .normalize('NFD') - .replace(/[^0-9a-z]/gi, '') - .toUpperCase()[0]; -} - type WorkspaceDuplicateFormProps = { policyID?: string; }; @@ -68,7 +62,7 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { } const newPolicyID = generatePolicyID(); setDuplicateWorkspaceData({policyID: newPolicyID, name, file: avatarFile}); - Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_DUPLICATE_SELECT_FEATURES.getRoute(policyID, ROUTES.WORKSPACES_LIST.route)); }, [policyID], ); @@ -84,22 +78,12 @@ function WorkspaceDuplicateForm({policyID}: WorkspaceDuplicateFormProps) { const stashedLocalAvatarImage = workspaceAvatar?.avatarUri ?? undefined; - const DefaultAvatar = useCallback( - () => ( - - ), - [workspaceAvatar?.avatarUri, workspaceNameFirstCharacter, styles.alignSelfCenter, styles.avatarXLarge, policyID], - ); + const DefaultAvatar = useWorkspaceConfirmationAvatar({ + policyID, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing cannot be used if left side can be empty string + source: stashedLocalAvatarImage || getDefaultWorkspaceAvatar(workspaceNameFirstCharacter), + name: workspaceNameFirstCharacter, + }); return ( <> diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 292bd9aabbd40..aa13a6275f04e 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -10,7 +10,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getAllValidConnectedIntegration, getDistanceRateCustomUnit, getMemberAccountIDsForWorkspace, getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import {getDistanceRateCustomUnit, getMemberAccountIDsForWorkspace, getPerDiemCustomUnit} from '@libs/PolicyUtils'; import {getReportFieldsByPolicyID} from '@libs/ReportUtils'; import Navigation from '@navigation/Navigation'; import {openPolicyCategoriesPage} from '@userActions/Policy/Category'; @@ -22,9 +22,10 @@ import {openPolicyReportFieldsPage} from '@userActions/Policy/ReportField'; import {openPolicyTagsPage} from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type {Rate} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {getWorkflowRules, getWorkspaceRules} from './utils'; +import {getAllValidConnectedIntegration, getWorkflowRules, getWorkspaceRules} from './utils'; type WorkspaceDuplicateFormProps = { policyID?: string; @@ -64,7 +65,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const [street1, street2] = (policy?.address?.addressStreet ?? '').split('\n'); const formattedAddress = !isEmptyObject(policy) && !isEmptyObject(policy.address) - ? `${street1?.trim()}, ${street2 ? `${street2.trim()}, ` : ''}${policy.address.city}, ${policy.address.state} ${policy.address.zipCode ?? ''}` + ? `, ${street1?.trim()}, ${street2 ? `${street2.trim()}, ` : ''}${policy.address.city}, ${policy.address.state} ${policy.address.zipCode ?? ''}` : ''; const items = useMemo(() => { @@ -150,7 +151,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm ? { translation: translate('workspace.common.perDiem'), value: 'perDiem', - alternateText: allRates ? `${allRates}${translate('workspace.common.perDiem').toLowerCase()}` : undefined, + alternateText: allRates ? `${allRates} ${translate('workspace.common.perDiem').toLowerCase()}` : undefined, } : undefined, // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -181,12 +182,15 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm ]); const listData: ListItem[] = useMemo(() => { - return items.map((option) => ({ - text: option.translation, - keyForList: option.value, - isSelected: selectedItems.includes(option.value), - alternateText: option.alternateText, - })); + return items.map((option) => { + const alternateText = option?.alternateText ? option.alternateText.trim().replace(/,$/, '') : undefined; + return { + text: option.translation, + keyForList: option.value, + isSelected: selectedItems.includes(option.value), + alternateText, + }; + }); }, [items, selectedItems]); const fetchWorkspaceRelatedData = useCallback(() => { @@ -203,14 +207,13 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm openPolicyWorkflowsPage(policyID); }, [policyID, allIds]); - const confirmDuplicateAndHideModal = useCallback(() => { - setIsDuplicateModalOpen(false); + const confirmDuplicate = useCallback(() => { if (!policy || !duplicateWorkspace?.name || !duplicateWorkspace?.policyID) { return; } duplicateWorkspaceAction(policy, { policyName: duplicateWorkspace.name, - policyID, + policyID: policy.id, targetPolicyID: duplicateWorkspace.policyID, welcomeNote: `${translate('workspace.duplicateWorkspace.welcomeNote')} ${duplicateWorkspace.name}`, parts: { @@ -229,7 +232,23 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm file: duplicateWorkspace?.file, }); Navigation.closeRHPFlow(); - }, [duplicateWorkspace?.file, duplicateWorkspace?.name, duplicateWorkspace?.policyID, policy, policyID, selectedItems, translate]); + }, [duplicateWorkspace?.file, duplicateWorkspace?.name, duplicateWorkspace?.policyID, policy, selectedItems, translate]); + + const confirmDuplicateAndHideModal = useCallback(() => { + setIsDuplicateModalOpen(false); + if (!policy || !duplicateWorkspace?.name || !duplicateWorkspace?.policyID) { + return; + } + confirmDuplicate(); + }, [confirmDuplicate, duplicateWorkspace?.name, duplicateWorkspace?.policyID, policy]); + + const onConfirmSelectList = useCallback(() => { + if (!totalMembers || totalMembers < 2 || !selectedItems.includes('members')) { + confirmDuplicate(); + return; + } + setIsDuplicateModalOpen(true); + }, [confirmDuplicate, selectedItems, totalMembers]); const updateSelectedItems = useCallback( (listItem: ListItem) => { @@ -280,7 +299,10 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm return ( <> - + Navigation.goBack(ROUTES.WORKSPACE_DUPLICATE.getRoute(policyID, ROUTES.WORKSPACES_LIST.route)) : undefined} + title={translate('workspace.common.duplicateWorkspace')} + /> <> {translate('workspace.duplicateWorkspace.selectFeatures')} @@ -296,7 +318,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm addBottomSafeAreaPadding showConfirmButton confirmButtonText={translate('common.next')} - onConfirm={() => setIsDuplicateModalOpen(true)} + onConfirm={onConfirmSelectList} /> diff --git a/src/pages/workspace/duplicate/utils.ts b/src/pages/workspace/duplicate/utils.ts index 05a5985930c81..aa4a78b79e705 100644 --- a/src/pages/workspace/duplicate/utils.ts +++ b/src/pages/workspace/duplicate/utils.ts @@ -2,8 +2,10 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {getCorrectedAutoReportingFrequency, getWorkflowApprovalsUnavailable, hasVBBA} from '@libs/PolicyUtils'; import {getAutoReportingFrequencyDisplayNames} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; import type {AutoReportingFrequencyKey} from '@pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage'; +import {isAuthenticationError} from '@userActions/connections'; import CONST from '@src/CONST'; import type {Policy} from '@src/types/onyx'; +import type {ConnectionName} from '@src/types/onyx/Policy'; function getWorkspaceRules(policy: Policy | undefined, translate: LocaleContextProps['translate']) { const workflowApprovalsUnavailable = getWorkflowApprovalsUnavailable(policy); @@ -68,4 +70,10 @@ function getWorkflowRules(policy: Policy | undefined, translate: LocaleContextPr return total.length > 0 ? total : null; } -export {getWorkspaceRules, getWorkflowRules}; +function getAllValidConnectedIntegration(policy: Policy | undefined, accountingIntegrations?: ConnectionName[]) { + return (accountingIntegrations ?? Object.values(CONST.POLICY.CONNECTIONS.NAME)).filter( + (integration) => !!policy?.connections?.[integration] && !isAuthenticationError(policy, integration), + ); +} + +export {getWorkspaceRules, getWorkflowRules, getAllValidConnectedIntegration}; From 58fbead1d573a1198955c6c216a45d5dde3b9dfa Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 14 Aug 2025 13:41:02 +0300 Subject: [PATCH 25/29] fix ts --- src/hooks/useWorkspaceConfirmationAvatar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/hooks/useWorkspaceConfirmationAvatar.tsx b/src/hooks/useWorkspaceConfirmationAvatar.tsx index a2c9b541a1b59..1420dda5ea56a 100644 --- a/src/hooks/useWorkspaceConfirmationAvatar.tsx +++ b/src/hooks/useWorkspaceConfirmationAvatar.tsx @@ -1,10 +1,11 @@ import React, {useCallback} from 'react'; import Avatar from '@components/Avatar'; import * as Expensicons from '@components/Icon/Expensicons'; +import type {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; import useThemeStyles from './useThemeStyles'; -function useWorkspaceConfirmationAvatar({policyID, source, name}: {policyID: string; source: string; name: string}) { +function useWorkspaceConfirmationAvatar({policyID, source, name}: {policyID: string; source: AvatarSource; name: string}) { const styles = useThemeStyles(); return useCallback( From d2529ea3d6ac6634d8264ffa58daa4e63c3edacd Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Thu, 14 Aug 2025 13:46:26 +0300 Subject: [PATCH 26/29] fix ts --- src/hooks/useWorkspaceConfirmationAvatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useWorkspaceConfirmationAvatar.tsx b/src/hooks/useWorkspaceConfirmationAvatar.tsx index 1420dda5ea56a..90967f4a92e96 100644 --- a/src/hooks/useWorkspaceConfirmationAvatar.tsx +++ b/src/hooks/useWorkspaceConfirmationAvatar.tsx @@ -5,7 +5,7 @@ import type {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; import useThemeStyles from './useThemeStyles'; -function useWorkspaceConfirmationAvatar({policyID, source, name}: {policyID: string; source: AvatarSource; name: string}) { +function useWorkspaceConfirmationAvatar({policyID, source, name}: {policyID: string | undefined; source: AvatarSource; name: string}) { const styles = useThemeStyles(); return useCallback( From bc898da30ffa50eeef8928cc5b22861d69757577 Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Mon, 18 Aug 2025 13:46:13 +0300 Subject: [PATCH 27/29] updates after review --- src/languages/de.ts | 6 ++--- src/languages/en.ts | 6 ++--- src/languages/es.ts | 6 ++--- src/languages/fr.ts | 6 ++--- src/languages/it.ts | 6 ++--- src/languages/ja.ts | 6 ++--- src/languages/nl.ts | 6 ++--- src/languages/pl.ts | 6 ++--- src/languages/pt-BR.ts | 6 ++--- src/languages/zh-hans.ts | 6 ++--- src/libs/actions/Policy/Policy.ts | 22 +++++++++++++++++++ .../WorkspaceDuplicateSelectFeaturesForm.tsx | 1 - 12 files changed, 52 insertions(+), 31 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 509ecef5b1bfa..b518aff272358 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4866,13 +4866,13 @@ const translations = { title: 'Benennen Sie Ihren neuen Arbeitsbereich', selectFeatures: 'Auswählen der zu kopierenden Features', whichFeatures: 'Welche Funktionen möchten Sie in Ihren neuen Arbeitsbereich kopieren?', - confirmDuplicate: '\n\nMöchten Sie wirklich fortfahren?', + confirmDuplicate: '\n\nMöchten Sie fortfahren?', categories: 'Kategorien und Ihre Auto-Kategorisierungsregeln', reimbursementAccount: 'Erstattungskonto', delayedSubmission: 'verspätete Einreichung', welcomeNote: 'Bitte beginnen Sie mit der Nutzung meines neuen Arbeitsbereichs', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Sie sind dabei, ${newWorkspaceName ?? ''} mit den ${totalMembers ?? 0} Personen zu teilen, die ${oldWorkspaceName ?? ''} mit ihnen geteilt haben. Bitte beachten Sie, dass jede dieser Personen eine Benachrichtigung erhält, dass sie sich nun im ${newWorkspaceName ?? ''} befindet.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Sie sind dabei, ${newWorkspaceName ?? ''} zu erstellen und mit ${totalMembers ?? 0} Mitgliedern aus dem ursprünglichen Arbeitsbereich zu teilen.`, }, emptyWorkspace: { title: 'Erstellen Sie einen Arbeitsbereich', diff --git a/src/languages/en.ts b/src/languages/en.ts index afecf2f936a5b..524df9e8b762e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4849,13 +4849,13 @@ const translations = { title: 'Name your new workspace', selectFeatures: 'Select features to copy', whichFeatures: 'Which features do you want to copy over to your new workspace?', - confirmDuplicate: '\n\nAre you sure you want to proceed?', + confirmDuplicate: '\n\nDo you want to continue?', categories: 'categories and your auto-categorization rules', reimbursementAccount: 'reimbursement account', welcomeNote: 'Please start using my new workspace', delayedSubmission: 'delayed submission', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `You are about to share ${newWorkspaceName ?? ''} this with the ${totalMembers ?? 0} people who have ${oldWorkspaceName ?? ''} shared with them. Please be aware that each of these people will receive a notification informing them that they are now on the ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `You’re about to create and share ${newWorkspaceName ?? ''} with ${totalMembers ?? 0} members from the original workspace.`, }, emptyWorkspace: { title: 'Create a workspace', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6180e7bbd0097..5e1bbefddafe7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4859,13 +4859,13 @@ const translations = { title: 'Nombra tu nuevo espacio de trabajo', selectFeatures: 'Selecciona las funciones a copiar', whichFeatures: '¿Qué funciones deseas copiar a tu nuevo espacio de trabajo?', - confirmDuplicate: '\n\n¿Estás seguro de que deseas continuar?', + confirmDuplicate: '\n\n¿Quieres continuar?', categories: 'categorías y tus reglas de auto-categorización', reimbursementAccount: 'cuenta de reembolso', delayedSubmission: 'presentación retrasada', welcomeNote: 'Por favor, comience a utilizar mi nuevo espacio de trabajo.', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Estás a punto de compartir ${newWorkspaceName ?? ''} con las ${totalMembers ?? 0} personas que ya tienen acceso a ${oldWorkspaceName ?? ''}. Ten en cuenta que cada una de estas personas recibirá una notificación informándoles que ahora forman parte de ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Estás a punto de crear y compartir ${newWorkspaceName ?? ''} con ${totalMembers ?? 0} miembros del espacio de trabajo original.`, }, emptyWorkspace: { title: 'Crea un espacio de trabajo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index b7ef5f067be53..9e613e4e59312 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4882,13 +4882,13 @@ const translations = { title: 'Nombra tu nuevo espacio de trabajo', selectFeatures: 'Selecciona las funciones que quieres copiar', whichFeatures: '¿Qué funciones quieres copiar a tu nuevo espacio de trabajo?', - confirmDuplicate: '\n\n¿Estás seguro de que quieres continuar?', + confirmDuplicate: '\n\nVoulez-vous continuer?', categories: 'Categorías y tus reglas de categorización automática', reimbursementAccount: 'Cuenta de reembolso', delayedSubmission: 'Envío retrasado', welcomeNote: 'Empieza a usar mi nuevo espacio de trabajo', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Estás a punto de compartir ${newWorkspaceName ?? ''} con las ${totalMembers ?? 0} personas que han compartido ${oldWorkspaceName ?? ''} con ellas. Tenga en cuenta que cada una de estas personas recibirá una notificación informándoles que ahora están en ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Vous êtes sur le point de créer et de partager ${newWorkspaceName ?? ''} avec ${totalMembers ?? 0} membres de l'espace de travail d'origine.`, }, emptyWorkspace: { title: 'Créer un espace de travail', diff --git a/src/languages/it.ts b/src/languages/it.ts index e477007dbdc3c..3cc7e10e940eb 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4881,13 +4881,13 @@ const translations = { title: 'Assegna un nome al tuo nuovo spazio di lavoro', selectFeatures: 'Seleziona le funzionalità da copiare', whichFeatures: 'Quali funzionalità vuoi copiare nel tuo nuovo spazio di lavoro?', - confirmDuplicate: '\n\nSei sicuro di voler procedere?', + confirmDuplicate: '\n\nVuoi continuare?', categories: 'Categorie e regole di categorizzazione automatica', reimbursementAccount: 'Account di rimborso', delayedSubmission: 'Invio ritardato', welcomeNote: 'Inizia a utilizzare il mio nuovo spazio di lavoro', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Stai per condividere ${newWorkspaceName ?? ''} questo con le ${totalMembers ?? 0} persone con cui hai condiviso ${oldWorkspaceName ?? ''}. Tieni presente che ciascuna di queste persone riceverà una notifica che la informerà che ora si trova su ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Stai per creare e condividere ${newWorkspaceName ?? ''} con ${totalMembers ?? 0} membri dall'area di lavoro originale.`, }, emptyWorkspace: { title: "Crea un'area di lavoro", diff --git a/src/languages/ja.ts b/src/languages/ja.ts index b6d4e6781f361..14f843e6471f0 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4860,13 +4860,13 @@ const translations = { title: '新しいワークスペースに名前を付けてください', selectFeatures: 'コピーする機能を選択してください', whichFeatures: '新しいワークスペースにコピーする機能はどれですか?', - confirmDuplicate: '\n\n続行してもよろしいですか?', + confirmDuplicate: '\n\n続行しますか?', categories: 'カテゴリと自動分類ルール', reimbursementAccount: '払い戻し口座', delayedSubmission: '遅延送信', welcomeNote: '新しいワークスペースの使用を開始してください', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `${newWorkspaceName ?? ''} を、${oldWorkspaceName ?? ''} を共有している ${totalMembers ?? 0} 人と共有しようとしています。これらの各ユーザーには、現在 ${newWorkspaceName ?? ''}. にいることを知らせる通知が届きますのでご注意ください`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `${newWorkspaceName ?? ''} を作成し、元のワークスペースの ${totalMembers ?? 0} 人のメンバーと共有しようとしています。`, }, emptyWorkspace: { title: 'ワークスペースを作成', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index b324d232b2396..d712ad7c8e114 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4883,13 +4883,13 @@ const translations = { title: 'Geef je nieuwe werkruimte een naam', selectFeatures: 'Selecteer te kopiëren functies', whichFeatures: 'Welke functies wil je kopiëren naar je nieuwe werkruimte?', - confirmDuplicate: '\n\nWeet je zeker dat je wilt doorgaan?', + confirmDuplicate: '\n\nWil je doorgaan?', categories: 'categorieën en je regels voor automatische categorisatie', reimbursementAccount: 'vergoedingsrekening', delayedSubmission: 'vertraagde indiening', welcomeNote: 'Ga aan de slag met mijn nieuwe werkruimte', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Je staat op het punt om ${newWorkspaceName ?? ''} dit te delen met de ${totalMembers ?? 0} mensen met wie ${oldWorkspaceName ?? ''} gedeeld is. Houd er rekening mee dat elk van deze personen een melding ontvangt met de mededeling dat ze nu op ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Je staat op het punt om ${newWorkspaceName ?? ''} te maken en te delen met ${totalMembers ?? 0} leden uit de oorspronkelijke werkruimte.`, }, emptyWorkspace: { title: 'Maak een werkruimte aan', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index f4f8887d709fa..485a73055ed3d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4872,13 +4872,13 @@ const translations = { title: 'Nazwij swój nowy obszar roboczy', selectFeatures: 'Wybierz funkcje do skopiowania', whichFeatures: 'Które funkcje chcesz skopiować do nowego obszaru roboczego?', - confirmDuplicate: '\n\nCzy na pewno chcesz kontynuować?', + confirmDuplicate: '\n\nCzy chcesz kontynuować?', categories: 'kategorie i zasady automatycznej kategoryzacji', reimbursementAccount: 'konto zwrotu', delayedSubmission: 'opóźnione przesłanie', welcomeNote: 'Proszę rozpocząć korzystanie z mojego nowego obszaru roboczego', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Zamierzasz udostępnić ${newWorkspaceName ?? ''} to ${totalMembers ?? 0} osobom, które mają ${oldWorkspaceName ?? ''} udostępniono im. Pamiętaj, że każda z tych osób otrzyma powiadomienie informujące, że znajduje się teraz w ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Zamierzasz utworzyć i udostępnić ${newWorkspaceName ?? ''} członkom ${totalMembers ?? 0} z oryginalnej przestrzeni roboczej.`, }, emptyWorkspace: { title: 'Utwórz przestrzeń roboczą', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0319bdf86b09b..08594cc083504 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4878,13 +4878,13 @@ const translations = { title: 'Nomeie seu novo espaço de trabalho', selectFeatures: 'Selecione os recursos a serem copiados', whichFeatures: 'Quais recursos você deseja copiar para o seu novo espaço de trabalho?', - confirmDuplicate: '\n\nTem certeza de que deseja prosseguir?', + confirmDuplicate: '\n\nVocê quer continuar?', categories: 'categorias e suas regras de categorização automática', reimbursementAccount: 'conta de reembolso', delayedSubmission: 'envio atrasado', welcomeNote: 'Comece a usar meu novo espaço de trabalho', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `Você está prestes a compartilhar ${newWorkspaceName ?? ''} com as ${totalMembers ?? 0} pessoas que compartilharam ${oldWorkspaceName ?? ''} com elas. Esteja ciente de que cada uma dessas pessoas receberá uma notificação informando que agora estão no ${newWorkspaceName ?? ''}.`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `Você está prestes a criar e compartilhar ${newWorkspaceName ?? ''} com ${totalMembers ?? 0} membros do espaço de trabalho original.`, }, emptyWorkspace: { title: 'Criar um espaço de trabalho', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index ddc71648b4a7c..5eccebbf2fcab 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4796,13 +4796,13 @@ const translations = { title: '命名您的新工作区', selectFeatures: '选择要复制的功能', whichFeatures: '您想要将哪些功能复制到您的新工作区?', - confirmDuplicate: '\n\n您确定要继续吗?', + confirmDuplicate: '\n\n您想继续吗?', categories: '类别和您的自动分类规则', reimbursementAccount: '报销账户', delayedSubmission: '延迟提交', welcomeNote: '请开始使用我的新工作区', - confirmTitle: ({newWorkspaceName, oldWorkspaceName, totalMembers}: {newWorkspaceName?: string; oldWorkspaceName?: string; totalMembers?: number}) => - `您即将与已共享 ${oldWorkspaceName ?? ''} 的 ${totalMembers ?? 0} 位用户共享此 ${newWorkspaceName ?? ''}。请注意,这些人都会收到一条通知,告知他们现在位于 ${newWorkspaceName ?? ''}。`, + confirmTitle: ({newWorkspaceName, totalMembers}: {newWorkspaceName?: string; totalMembers?: number}) => + `您即将创建并与原始工作区中的 ${totalMembers ?? 0} 名成员共享 ${newWorkspaceName ?? ''}。`, }, emptyWorkspace: { title: '创建一个工作区', diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 1c7705dbc45fc..a1f02dc7749bd 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -2498,6 +2498,15 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp pendingChatMembers, } = ReportUtils.buildOptimisticWorkspaceChats(targetPolicyID, policyName); const isMemberOptionSelected = parts?.people; + const isReportsOptionSelected = parts?.reports; + const isConnectionsOptionSelected = parts?.connections; + const isCategoriesOptionSelected = parts?.categories; + const isTaxesOptionSelected = parts?.taxes; + const isTagsOptionSelected = parts?.tags; + const isInvoicesOptionSelected = parts?.invoices; + const isCustomUnitsOptionSelected = parts?.customUnits; + const isRulesOptionSelected = parts?.expenses; + const isWorkflowsOptionSelected = parts?.exportLayouts; const policyMemberAccountIDs = isMemberOptionSelected ? Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList, false, false)) : []; const {customUnitID, customUnitRateID} = buildOptimisticDistanceRateCustomUnits(policy?.outputCurrency); @@ -2513,9 +2522,22 @@ function buildDuplicatePolicyData(policy: Policy, options: DuplicatePolicyDataOp key: `${ONYXKEYS.COLLECTION.POLICY}${targetPolicyID}`, value: { ...policy, + areCategoriesEnabled: isCategoriesOptionSelected, + areTagsEnabled: isTagsOptionSelected, + areDistanceRatesEnabled: isCustomUnitsOptionSelected, + areInvoicesEnabled: isInvoicesOptionSelected, + areRulesEnabled: isRulesOptionSelected, + areWorkflowsEnabled: isWorkflowsOptionSelected, + areReportFieldsEnabled: isReportsOptionSelected, + areConnectionsEnabled: isConnectionsOptionSelected, + tax: isTaxesOptionSelected ? policy?.tax : undefined, employeeList: isMemberOptionSelected ? policy.employeeList : {[policy.owner]: policy?.employeeList?.[policy.owner]}, id: targetPolicyID, name: policyName, + fieldList: isReportsOptionSelected ? policy?.fieldList : undefined, + connections: isConnectionsOptionSelected ? policy?.connections : undefined, + customUnits: isCustomUnitsOptionSelected ? policy?.customUnits : undefined, + taxRates: isTaxesOptionSelected ? policy?.taxRates : undefined, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, pendingFields: { autoReporting: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index aa13a6275f04e..49f593280f05e 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -332,7 +332,6 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm {translate('workspace.duplicateWorkspace.confirmTitle', { newWorkspaceName: duplicateWorkspace?.name, - oldWorkspaceName: policy?.name, totalMembers, })} From 4d473a944fa54da6d5c33102a890902ebeb2e66f Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Tue, 19 Aug 2025 12:39:47 +0300 Subject: [PATCH 28/29] updates after review --- .../duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 49f593280f05e..40993aec7bb2e 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -80,7 +80,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm { translation: translate('workspace.common.profile'), value: 'overview', - alternateText: `${policy?.name}, ${policy?.outputCurrency} ${translate('common.currency')}, ${formattedAddress}`, + alternateText: `${policy?.outputCurrency} ${translate('common.currency')}, ${formattedAddress}`, }, totalMembers > 1 ? { From e99ded374227cdeba2db48c47b65b50ee9db35db Mon Sep 17 00:00:00 2001 From: Nicolay Arefyeu Date: Wed, 20 Aug 2025 10:16:27 +0300 Subject: [PATCH 29/29] updates after review --- .../duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 40993aec7bb2e..f3d548dce060a 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -317,7 +317,7 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm isAlternateTextMultilineSupported addBottomSafeAreaPadding showConfirmButton - confirmButtonText={translate('common.next')} + confirmButtonText={translate('common.continue')} onConfirm={onConfirmSelectList} />