From 316510452b0aa17106810c5ff9a4e0fd5fe61023 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 23 Aug 2024 15:07:45 +0200 Subject: [PATCH 01/13] Add import flow to Members page --- src/ROUTES.ts | 8 + src/SCREENS.ts | 2 + src/languages/en.ts | 4 + src/languages/es.ts | 4 + .../parameters/ImportMembersSpreadsheet.ts | 10 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../ModalStackNavigators/index.tsx | 2 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 + src/libs/Navigation/linkingConfig/config.ts | 6 + src/libs/Navigation/types.ts | 6 + src/libs/actions/Policy/Member.ts | 49 ++++++ src/pages/workspace/WorkspaceMembersPage.tsx | 35 +++- .../workspace/WorkspacePageWithSections.tsx | 16 +- .../workspace/members/ImportMembersPage.tsx | 21 +++ .../workspace/members/ImportedMembersPage.tsx | 158 ++++++++++++++++++ 16 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 src/libs/API/parameters/ImportMembersSpreadsheet.ts create mode 100644 src/pages/workspace/members/ImportMembersPage.tsx create mode 100644 src/pages/workspace/members/ImportedMembersPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0e9ff6bfad9d..f3f2aaa1ec5a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -700,6 +700,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/members', getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const, }, + WORKSPACE_MEMBERS_IMPORT: { + route: 'settings/workspaces/:policyID/members/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/members/import` as const, + }, + WORKSPACE_MEMBERS_IMPORTED: { + route: 'settings/workspaces/:policyID/members/imported', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/members/imported` as const, + }, POLICY_ACCOUNTING: { route: 'settings/workspaces/:policyID/accounting', getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9aa2c27c4905..f6e5a16a989f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -380,6 +380,8 @@ const SCREENS = { INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website', TRAVEL: 'Workspace_Travel', MEMBERS: 'Workspace_Members', + MEMBERS_IMPORT: 'Members_Import', + MEMBERS_IMPORTED: 'Members_Imported', INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', CATEGORIES: 'Workspace_Categories', diff --git a/src/languages/en.ts b/src/languages/en.ts index 988aba2b6333..9243ea0c29dc 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -678,6 +678,7 @@ export default { singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`, importSuccessfullTitle: 'Import successful', importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'), + importMembersSuccessfullDescription: (members: number) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'), importFailedTitle: 'Oops! There was an issue with your upload.', importFailedDescription: 'Please ensure all fields are correctly filled and try again. If the problem persists, please reach out to Concierge.', }, @@ -3092,6 +3093,9 @@ export default { addedWithPrimary: 'Some members were added with their primary logins.', invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`, membersListTitle: 'Directory of all workspace members.', + importMembers: 'Import members', + importedMembersMessage: (columnCounts: number) => + `We found *${columnCounts} columns* in your spreadsheet. Select *Email* next to the column that contains emails. You can also select *Role* next to the column that sets users roles.`, }, card: { header: 'Unlock free Expensify Cards', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5cc9fa39ee7e..70d15133e85a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -672,6 +672,7 @@ export default { importFailedDescription: 'La importación ha fallado. Por favor, revise los errores e inténtelo de nuevo', importFailedTitle: 'Importación fallida', importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'), + importMembersSuccessfullDescription: (members: number) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'), importSuccessfullTitle: 'Importar categorías', }, receipt: { @@ -3140,6 +3141,9 @@ export default { addedWithPrimary: 'Se agregaron algunos miembros con sus nombres de usuario principales.', invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.', + importMembers: 'Importar miembros', + importedMembersMessage: (columnCounts: number) => + `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Email* junto a la columna que contiene correos electrónicos. También puede seleccionar *Role* junto a la columna que establece los roles de los usuarios.`, }, accounting: { settings: 'configuración', diff --git a/src/libs/API/parameters/ImportMembersSpreadsheet.ts b/src/libs/API/parameters/ImportMembersSpreadsheet.ts new file mode 100644 index 000000000000..166e43c0510f --- /dev/null +++ b/src/libs/API/parameters/ImportMembersSpreadsheet.ts @@ -0,0 +1,10 @@ +type ImportMembersSpreadsheetParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array<{email: string, role: string}> + */ + employees: string; +}; + +export default ImportMembersSpreadsheetParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 7f1167e6655f..edac7f5669c4 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -280,3 +280,4 @@ export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPagePa export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet'; +export type {default as ImportMembersSpreadsheetParams} from './ImportMembersSpreadsheet'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e2b6c958ecab..4133bcaf35dd 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -131,6 +131,7 @@ const WRITE_COMMANDS = { SET_POLICY_TAGS_ENABLED: 'SetPolicyTagsEnabled', CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', IMPORT_CATEGORIES_SREADSHEET: 'ImportCategoriesSpreadsheet', + IMPORT_MEMBERS_SREADSHEET: 'ImportMembersSpreadsheet', RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory', CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', @@ -457,6 +458,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams; [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams; [WRITE_COMMANDS.IMPORT_CATEGORIES_SREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams; + [WRITE_COMMANDS.IMPORT_MEMBERS_SREADSHEET]: Parameters.ImportMembersSpreadsheetParams; [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ac74f8120fd9..616b67cd5c5e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -224,6 +224,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/RatePage').default, [SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT]: () => require('../../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage/UnitPage').default, [SCREENS.WORKSPACE.INVITE]: () => require('../../../../pages/workspace/WorkspaceInvitePage').default, + [SCREENS.WORKSPACE.MEMBERS_IMPORT]: () => require('../../../../pages/workspace/members/ImportMembersPage').default, + [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: () => require('../../../../pages/workspace/members/ImportedMembersPage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EXPENSES_FROM]: () => diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 881ac49f6dba..dc084662638f 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -12,6 +12,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS, SCREENS.WORKSPACE.OWNER_CHANGE_ERROR, SCREENS.WORKSPACE.OWNER_CHANGE_ERROR, + SCREENS.WORKSPACE.MEMBERS_IMPORT, + SCREENS.WORKSPACE.MEMBERS_IMPORTED, ], [SCREENS.WORKSPACE.WORKFLOWS]: [ SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b2d25497e0c5..a45922a8cc2a 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -520,6 +520,12 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.INVITE]: { path: ROUTES.WORKSPACE_INVITE.route, }, + [SCREENS.WORKSPACE.MEMBERS_IMPORT]: { + path: ROUTES.WORKSPACE_MEMBERS_IMPORT.route, + }, + [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: { + path: ROUTES.WORKSPACE_MEMBERS_IMPORTED.route, + }, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: { path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 5c2fae4ac364..d3e98ad4b727 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -200,6 +200,12 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.INVITE]: { policyID: string; }; + [SCREENS.WORKSPACE.MEMBERS_IMPORT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: { + policyID: string; + }; [SCREENS.WORKSPACE.INVITE_MESSAGE]: { policyID: string; }; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 1b881f65c831..806576f1d673 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -10,6 +10,7 @@ import type { } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; +import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; import Parser from '@libs/Parser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -22,6 +23,7 @@ import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyEmplo import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import type {Attributes, Rate} from '@src/types/onyx/Policy'; +import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {createPolicyExpenseChats} from './Policy'; @@ -166,6 +168,36 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[] }); return announceRoomMembers; } +/** + * Updates the import spreadsheet data according to the result of the import + */ +function updateImportSpreadsheetData(membersLength: number) { + const onyxData: OnyxData = { + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importMembersSuccessfullDescription', membersLength)}, + }, + }, + ], + + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importFailedTitle'), prompt: translateLocal('spreadsheet.importFailedDescription')}, + }, + }, + ], + }; + + return onyxData; +} /** * Build optimistic data for removing users from the announcement room @@ -639,6 +671,22 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount API.write(WRITE_COMMANDS.ADD_MEMBERS_TO_WORKSPACE, params, {optimisticData, successData, failureData}); } +type PolicyMember = { + email: string; + role: string; +}; + +function importPolicyMembers(policyID: string, members: PolicyMember[]) { + const onyxData = updateImportSpreadsheetData(members.length); + + const parameters = { + policyID, + employees: JSON.stringify([...members.map((member) => ({email: member.email, role: member.role}))]), + }; + + API.write(WRITE_COMMANDS.IMPORT_MEMBERS_SREADSHEET, parameters, onyxData); +} + /** * Invite member to the specified policyID * Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details @@ -849,6 +897,7 @@ export { acceptJoinRequest, declineJoinRequest, isApprover, + importPolicyMembers, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index d730dde02a67..631de12069a2 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -26,6 +26,7 @@ import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; @@ -68,10 +69,12 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); const {isOffline} = useNetwork(); + const {windowWidth} = useWindowDimensions(); const prevIsOffline = usePrevious(isOffline); const accountIDs = useMemo(() => Object.values(policyMemberEmailsToAccountIDs ?? {}).map((accountID) => Number(accountID)), [policyMemberEmailsToAccountIDs]); const prevAccountIDs = usePrevious(accountIDs); const textInputRef = useRef(null); + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; const prevPersonalDetails = usePrevious(personalDetails); const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); @@ -506,7 +509,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson return null; } return ( - + {(shouldUseNarrowLayout ? canSelectMultiple : selectedEmployees.length > 0) ? ( shouldAlwaysShowDropdownMenu @@ -534,6 +537,24 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson ); }; + const threeDotsMenuItems = useMemo(() => { + const menuItems = [ + { + icon: Expensicons.Table, + text: translate('common.importSpreadsheet'), + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID)); + }, + }, + ]; + + return menuItems; + }, [policyID, translate, isOffline]); + const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; return ( @@ -546,6 +567,9 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson testID={WorkspaceMembersPage.displayName} shouldShowLoading={false} shouldShowOfflineIndicatorInWideScreen + shouldShowThreeDotsButton + threeDotsMenuItems={threeDotsMenuItems} + threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} shouldShowNonAdmin onBackButtonPress={() => { if (selectionMode?.isEnabled) { @@ -559,6 +583,15 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson {() => ( <> {shouldUseNarrowLayout && {getHeaderButtons()}} + setIsOfflineModalVisible(false)} + title={translate('common.youAppearToBeOffline')} + prompt={translate('common.thisFeatureRequiresInternet')} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + & { shouldSkipVBBACall?: boolean; /** The text to display in the header */ @@ -60,9 +62,6 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & /** Option to show the loading page while the API is calling */ shouldShowLoading?: boolean; - /** Should show the back button. It is used when in RHP. */ - shouldShowBackButton?: boolean; - /** Whether the offline indicator should be shown in wide screen devices */ shouldShowOfflineIndicatorInWideScreen?: boolean; @@ -90,9 +89,6 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & /** Whether the page is loading, example any other API call in progres */ isLoading?: boolean; - - /** Callback to be called when the back button is pressed */ - onBackButtonPress?: () => void; }; function fetchData(policyID: string, skipVBBACal?: boolean) { @@ -126,6 +122,9 @@ function WorkspacePageWithSections({ shouldShowNotFoundPage = false, isLoading: isPageLoading = false, onBackButtonPress, + shouldShowThreeDotsButton, + threeDotsMenuItems, + threeDotsAnchorPosition, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); const policyID = route.params?.policyID ?? '-1'; @@ -189,6 +188,9 @@ function WorkspacePageWithSections({ shouldShowBackButton={shouldUseNarrowLayout || shouldShowBackButton} icon={icon ?? undefined} style={styles.headerBarDesktopHeight} + shouldShowThreeDotsButton={shouldShowThreeDotsButton} + threeDotsMenuItems={threeDotsMenuItems} + threeDotsAnchorPosition={threeDotsAnchorPosition} > {headerContent} diff --git a/src/pages/workspace/members/ImportMembersPage.tsx b/src/pages/workspace/members/ImportMembersPage.tsx new file mode 100644 index 000000000000..6fff3085b472 --- /dev/null +++ b/src/pages/workspace/members/ImportMembersPage.tsx @@ -0,0 +1,21 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import ImportSpreedsheet from '@components/ImportSpreadsheet'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ImportMembersPageProps = StackScreenProps; + +function ImportMembersPage({route}: ImportMembersPageProps) { + const policyID = route.params.policyID; + + return ( + + ); +} + +export default ImportMembersPage; diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx new file mode 100644 index 000000000000..6dfec0b4a06e --- /dev/null +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -0,0 +1,158 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ColumnRole} from '@components/ImportColumn'; +import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import {closeImportPage} from '@libs/actions/ImportSpreadsheet'; +import {importPolicyMembers} from '@libs/actions/Policy/Member'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ImportedMembersPageProps = StackScreenProps; + +function numberToColumn(index: number) { + let column = ''; + let number = index; + while (number >= 0) { + column = String.fromCharCode((number % 26) + 65) + column; + number = Math.floor(number / 26) - 1; + } + return column; +} + +function generateColumnNames(length: number) { + return Array.from({length}, (_, i) => numberToColumn(i)); +} + +function findDuplicate(array: string[]): string | null { + const frequencyCounter: Record = {}; + + for (const item of array) { + if (item !== CONST.CSV_IMPORT_COLUMNS.IGNORE) { + if (frequencyCounter[item]) { + return item; + } + frequencyCounter[item] = (frequencyCounter[item] || 0) + 1; + } + } + + return null; +} + +function ImportedMembersPage({route}: ImportedMembersPageProps) { + const {translate} = useLocalize(); + const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); + const [isImporting, setIsImporting] = useState(false); + const [containsHeader, setContainsHeader] = useState(false); + const policyID = route.params.policyID; + const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0); + + const columnRoles: ColumnRole[] = [ + {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE}, + {text: translate('common.email'), value: CONST.CSV_IMPORT_COLUMNS.EMAIL, isRequired: true}, + {text: translate('common.role'), value: CONST.CSV_IMPORT_COLUMNS.ROLE}, + ]; + + const requiredColumns = columnRoles.filter((role) => role.isRequired).map((role) => role); + + const validate = useCallback(() => { + const columns = Object.values(spreadsheet?.columns ?? {}); + let errors: Record = {}; + + if (!requiredColumns.every((requiredColumn) => columns.includes(requiredColumn.value))) { + // eslint-disable-next-line rulesdir/prefer-early-return + requiredColumns.forEach((requiredColumn) => { + if (!columns.includes(requiredColumn.value)) { + errors.required = translate('spreadsheet.fieldNotMapped', requiredColumn.text); + } + }); + } else { + const duplicate = findDuplicate(columns); + if (duplicate) { + errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicate); + } else { + errors = {}; + } + } + + return errors; + }, [requiredColumns, spreadsheet?.columns, translate]); + + const importMembers = useCallback(() => { + validate(); + const columns = Object.values(spreadsheet?.columns ?? {}); + const membersEmailsColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.EMAIL); + const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); + const membersEmails = spreadsheet?.data[membersEmailsColumn].map((email) => email); + const membersRoles = membersRolesColumn !== -1 ? spreadsheet?.data[membersRolesColumn].map((role) => role) : []; + const members = membersEmails?.slice(containsHeader ? 1 : 0).map((email, index) => { + let role: string = CONST.POLICY.ROLE.USER; + if (membersRolesColumn !== -1 && membersRoles?.[containsHeader ? index + 1 : index]) { + role = membersRoles?.[containsHeader ? index + 1 : index]; + } + + return { + email, + role, + }; + }); + + if (members) { + setIsImporting(true); + importPolicyMembers(policyID, members); + } + }, [validate, spreadsheet, containsHeader, policyID]); + + const spreadsheetColumns = spreadsheet?.data; + if (!spreadsheetColumns) { + return; + } + + return ( + + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID))} + /> + + + { + setIsImporting(false); + closeImportPage(); + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); + }} + confirmText={translate('common.buttonConfirm')} + shouldShowCancelButton={false} + /> + + ); +} + +ImportedMembersPage.displayName = 'ImportedMembersPage'; + +export default ImportedMembersPage; From f4b6f46e3c14176ab1c58c8fde5b679b67b721cd Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 30 Aug 2024 13:47:36 +0200 Subject: [PATCH 02/13] Fix members import --- src/CONST.ts | 1 + .../workspace/members/ImportedMembersPage.tsx | 16 +++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 495c6efcd9dd..f8640731e177 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5503,6 +5503,7 @@ const CONST = { ICON_HEIGHT: 160, CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories', + MEMBERS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles#import-a-group-of-members', }, } as const; diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 6dfec0b4a06e..ff2af7afc780 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -51,9 +51,10 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const {translate} = useLocalize(); const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); const [isImporting, setIsImporting] = useState(false); - const [containsHeader, setContainsHeader] = useState(false); + const [isValidationEnabled, setIsValidationEnabled] = useState(false); const policyID = route.params.policyID; const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0); + const {containsHeader = true} = spreadsheet ?? {}; const columnRoles: ColumnRole[] = [ {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE}, @@ -87,7 +88,13 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { }, [requiredColumns, spreadsheet?.columns, translate]); const importMembers = useCallback(() => { - validate(); + setIsValidationEnabled(true); + + const errors = validate(); + if (Object.keys(errors).length > 0) { + return; + } + const columns = Object.values(spreadsheet?.columns ?? {}); const membersEmailsColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.EMAIL); const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); @@ -127,14 +134,13 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { /> Date: Tue, 10 Sep 2024 10:52:47 +0200 Subject: [PATCH 03/13] ts fix --- src/libs/actions/Policy/Member.ts | 2 +- src/pages/workspace/WorkspaceMembersPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 806576f1d673..540fa5074e3f 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -684,7 +684,7 @@ function importPolicyMembers(policyID: string, members: PolicyMember[]) { employees: JSON.stringify([...members.map((member) => ({email: member.email, role: member.role}))]), }; - API.write(WRITE_COMMANDS.IMPORT_MEMBERS_SREADSHEET, parameters, onyxData); + API.write(WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET, parameters, onyxData); } /** diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 2b7c3bb053aa..cbcd8f02d6b5 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -542,7 +542,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson const menuItems = [ { icon: Expensicons.Table, - text: translate('common.importSpreadsheet'), + text: translate('spreadsheet.importSpreadsheet'), onSelected: () => { if (isOffline) { setIsOfflineModalVisible(true); From 8ca6b73ddda89e8ab282580a5d6115488a12c0d9 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 10 Sep 2024 12:01:32 +0200 Subject: [PATCH 04/13] adding fixes after categories --- src/pages/workspace/members/ImportedMembersPage.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index ff2af7afc780..b639b9926dc6 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -123,6 +123,12 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { return; } + const closeImportPageAndModal = () => { + setIsImporting(false); + closeImportPage(); + Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + }; + return ( { - setIsImporting(false); - closeImportPage(); - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); - }} + onConfirm={closeImportPageAndModal} + onCancel={closeImportPageAndModal} confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} /> From 5f98deb13259a4f351cc4cd84f2cd4bd05b8cc74 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 10 Sep 2024 17:40:30 +0200 Subject: [PATCH 05/13] add members export --- .../ExportMembersSpreadsheetParams.ts | 6 ++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Policy/Member.ts | 18 ++++++++++++++++++ src/pages/workspace/WorkspaceMembersPage.tsx | 14 +++++++++++++- 5 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 src/libs/API/parameters/ExportMembersSpreadsheetParams.ts diff --git a/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts b/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts new file mode 100644 index 000000000000..1d8dbf0dad47 --- /dev/null +++ b/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts @@ -0,0 +1,6 @@ +type ExportMemberssSpreadsheetParams = { + /** ID of the policy */ + policyID: string; +}; + +export default ExportMemberssSpreadsheetParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 5b126c494487..633927140b27 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -310,6 +310,7 @@ export type {default as UpdateQuickbooksOnlineAutoCreateVendorParams} from './Up export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet'; export type {default as ImportMembersSpreadsheetParams} from './ImportMembersSpreadsheet'; export type {default as ExportCategoriesSpreadsheetParams} from './ExportCategoriesSpreadsheet'; +export type {default as ExportMembersSpreadsheetParams} from './ExportCategoriesSpreadsheet'; export type {default as UpdateXeroGenericTypeParams} from './UpdateXeroGenericTypeParams'; export type {default as UpdateCardSettlementFrequencyParams} from './UpdateCardSettlementFrequencyParams'; export type {default as UpdateCardSettlementAccountParams} from './UpdateCardSettlementAccountParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8a45861accbf..4a776e663f85 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -145,6 +145,7 @@ const WRITE_COMMANDS = { IMPORT_MEMBERS_SPREADSHEET: 'ImportMembersSpreadsheet', IMPORT_CATEGORIES_SPREADSHEET: 'ImportCategoriesSpreadsheet', EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV', + EXPORT_MEMBERS_CSV: 'ExportMembersCSV', RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory', CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', @@ -506,6 +507,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET]: Parameters.ImportMembersSpreadsheetParams; [WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams; [WRITE_COMMANDS.EXPORT_CATEGORIES_CSV]: Parameters.ExportCategoriesSpreadsheetParams; + [WRITE_COMMANDS.EXPORT_MEMBERS_CSV]: Parameters.ExportMembersSpreadsheetParams; [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 540fa5074e3f..76368b073f78 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -9,9 +9,12 @@ import type { UpdateWorkspaceMembersRoleParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ApiUtils from '@libs/ApiUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; +import fileDownload from '@libs/fileDownload'; import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; +import enhanceParameters from '@libs/Network/enhanceParameters'; import Parser from '@libs/Parser'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PhoneNumber from '@libs/PhoneNumber'; @@ -882,6 +885,20 @@ function declineJoinRequest(reportID: string, reportAction: OnyxEntry { + formData.append(key, String(value)); + }); + + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_MEMBERS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); +} export { removeMembers, @@ -898,6 +915,7 @@ export { declineJoinRequest, isApprover, importPolicyMembers, + downloadMembersCSV, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index cbcd8f02d6b5..457ef91f5c46 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -37,6 +37,7 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {getDisplayNameForParticipant} from '@libs/ReportUtils'; +import * as Modal from '@userActions/Modal'; import * as Member from '@userActions/Policy/Member'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -545,12 +546,23 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson text: translate('spreadsheet.importSpreadsheet'), onSelected: () => { if (isOffline) { - setIsOfflineModalVisible(true); + Modal.close(() => setIsOfflineModalVisible(true)); return; } Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(policyID)); }, }, + { + icon: Expensicons.Download, + text: translate('spreadsheet.downloadCSV'), + onSelected: () => { + if (isOffline) { + Modal.close(() => setIsOfflineModalVisible(true)); + return; + } + Member.downloadMembersCSV(policyID); + }, + }, ]; return menuItems; From 6d0acbbce33e416367ca4bc09577721716b2f787 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 11 Sep 2024 09:43:11 +0200 Subject: [PATCH 06/13] Update src/libs/actions/Policy/Member.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Błażej Kustra <46095609+blazejkustra@users.noreply.github.com> --- src/libs/actions/Policy/Member.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 76368b073f78..5dde7e84057b 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -885,6 +885,7 @@ function declineJoinRequest(reportID: string, reportAction: OnyxEntry Date: Wed, 11 Sep 2024 09:52:03 +0200 Subject: [PATCH 07/13] review fixes --- src/libs/actions/Policy/Member.ts | 4 ++-- src/pages/workspace/members/ImportedMembersPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 5dde7e84057b..c5070b1e6487 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -174,7 +174,7 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[] /** * Updates the import spreadsheet data according to the result of the import */ -function updateImportSpreadsheetData(membersLength: number) { +function updateImportSpreadsheetData(membersLength: number): OnyxData { const onyxData: OnyxData = { successData: [ { @@ -684,7 +684,7 @@ function importPolicyMembers(policyID: string, members: PolicyMember[]) { const parameters = { policyID, - employees: JSON.stringify([...members.map((member) => ({email: member.email, role: member.role}))]), + employees: JSON.stringify(members.map((member) => ({email: member.email, role: member.role}))), }; API.write(WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET, parameters, onyxData); diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index b639b9926dc6..d8c7195cb0da 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -126,7 +126,7 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const closeImportPageAndModal = () => { setIsImporting(false); closeImportPage(); - Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); }; return ( From 333a89a178a8afb89f2429ea47da6054d695b4e1 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Fri, 13 Sep 2024 10:06:57 +0200 Subject: [PATCH 08/13] Migrate withOnyx on members page --- .../workspace/WorkspacePageWithSections.tsx | 32 ++++--------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index e8f18c7cee71..4f0a84cffd9c 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -3,7 +3,7 @@ import type {ReactNode} from 'react'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -21,23 +21,13 @@ import * as BankAccounts from '@userActions/BankAccounts'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; -import type {Policy, ReimbursementAccount, User} from '@src/types/onyx'; +import type {Policy} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; -type WorkspacePageWithSectionsOnyxProps = { - /** From Onyx */ - /** Bank account attached to free plan */ - reimbursementAccount: OnyxEntry; - - /** User Data from Onyx */ - user: OnyxEntry; -}; - type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & - WorkspacePageWithSectionsOnyxProps & Pick & { shouldSkipVBBACall?: boolean; @@ -114,13 +104,11 @@ function WorkspacePageWithSections({ headerText, policy, policyDraft, - reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, route, shouldUseScrollView = false, showLoadingAsFirstRender = true, shouldSkipVBBACall = true, shouldShowBackButton = false, - user, shouldShowLoading = true, shouldShowOfflineIndicatorInWideScreen = false, includeSafeAreaPaddingBottom = false, @@ -138,6 +126,9 @@ function WorkspacePageWithSections({ const policyID = route.params?.policyID ?? '-1'; useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); + const [user] = useOnyx(ONYXKEYS.USER); + const [reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; const achState = reimbursementAccount?.achData?.state ?? '-1'; @@ -148,7 +139,6 @@ function WorkspacePageWithSections({ const firstRender = useRef(showLoadingAsFirstRender); const isFocused = useIsFocused(); const prevPolicy = usePrevious(policy); - useEffect(() => { // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true firstRender.current = false; @@ -226,14 +216,4 @@ function WorkspacePageWithSections({ WorkspacePageWithSections.displayName = 'WorkspacePageWithSections'; -export default withPolicyAndFullscreenLoading( - withOnyx({ - user: { - key: ONYXKEYS.USER, - }, - // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM - reimbursementAccount: { - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - }, - })(WorkspacePageWithSections), -); +export default withPolicyAndFullscreenLoading(WorkspacePageWithSections); From 8af21ea96230ca0356e4fbe7d7b91422df1e25cf Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 17 Sep 2024 14:08:10 +0200 Subject: [PATCH 09/13] review comments --- src/pages/workspace/members/ImportedMembersPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index d8c7195cb0da..bbb8c65210ce 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -40,7 +40,7 @@ function findDuplicate(array: string[]): string | null { if (frequencyCounter[item]) { return item; } - frequencyCounter[item] = (frequencyCounter[item] || 0) + 1; + frequencyCounter[item] = 1; } } @@ -64,6 +64,8 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const requiredColumns = columnRoles.filter((role) => role.isRequired).map((role) => role); + // checks if all required columns are mapped and no column is mapped more than once + // returns found errors or empty object if both conditions are met const validate = useCallback(() => { const columns = Object.values(spreadsheet?.columns ?? {}); let errors: Record = {}; From 5c079e9e47ba3d278b9c1246207949357adeadaf Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 17 Sep 2024 17:23:40 +0200 Subject: [PATCH 10/13] fix typo --- src/libs/API/parameters/ExportMembersSpreadsheetParams.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts b/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts index 1d8dbf0dad47..04a114cddaf5 100644 --- a/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts +++ b/src/libs/API/parameters/ExportMembersSpreadsheetParams.ts @@ -1,6 +1,6 @@ -type ExportMemberssSpreadsheetParams = { +type ExportMembersSpreadsheetParams = { /** ID of the policy */ policyID: string; }; -export default ExportMemberssSpreadsheetParams; +export default ExportMembersSpreadsheetParams; From 92e6745783bcd846e29f9344215c3c95281dd04a Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Tue, 17 Sep 2024 17:27:57 +0200 Subject: [PATCH 11/13] functions moved to utils --- .../workspace/members/ImportedMembersPage.tsx | 30 +------------------ 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index bbb8c65210ce..4215991922c2 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -9,6 +9,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import {closeImportPage} from '@libs/actions/ImportSpreadsheet'; import {importPolicyMembers} from '@libs/actions/Policy/Member'; +import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import CONST from '@src/CONST'; @@ -18,35 +19,6 @@ import type SCREENS from '@src/SCREENS'; type ImportedMembersPageProps = StackScreenProps; -function numberToColumn(index: number) { - let column = ''; - let number = index; - while (number >= 0) { - column = String.fromCharCode((number % 26) + 65) + column; - number = Math.floor(number / 26) - 1; - } - return column; -} - -function generateColumnNames(length: number) { - return Array.from({length}, (_, i) => numberToColumn(i)); -} - -function findDuplicate(array: string[]): string | null { - const frequencyCounter: Record = {}; - - for (const item of array) { - if (item !== CONST.CSV_IMPORT_COLUMNS.IGNORE) { - if (frequencyCounter[item]) { - return item; - } - frequencyCounter[item] = 1; - } - } - - return null; -} - function ImportedMembersPage({route}: ImportedMembersPageProps) { const {translate} = useLocalize(); const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); From 656a3d778891bbce71133f278851788f9d8a09f5 Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 18 Sep 2024 12:57:17 +0200 Subject: [PATCH 12/13] resolve typecheck --- src/components/ImportSpreadsheetColumns.tsx | 6 +++--- src/libs/actions/Policy/Tag.ts | 2 +- src/pages/workspace/members/ImportedMembersPage.tsx | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/components/ImportSpreadsheetColumns.tsx b/src/components/ImportSpreadsheetColumns.tsx index 95a6ea4516ac..9ba0597bd3d6 100644 --- a/src/components/ImportSpreadsheetColumns.tsx +++ b/src/components/ImportSpreadsheetColumns.tsx @@ -40,7 +40,7 @@ type ImportSpreadsheetColumnsProps = { learnMoreLink?: string; }; -function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) { +function ImportSpreadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -101,6 +101,6 @@ function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles ); } -ImportSpreeadsheetColumns.displayName = 'ImportSpreeadsheetColumns'; +ImportSpreadsheetColumns.displayName = 'ImportSpreadsheetColumns'; -export default ImportSpreeadsheetColumns; +export default ImportSpreadsheetColumns; diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 6455e7fad947..b51918b37563 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -228,7 +228,7 @@ function importPolicyTags(policyID: string, tags: PolicyTag[]) { tags: JSON.stringify(tags.map((tag) => ({name: tag.name, enabled: tag.enabled, 'GL Code': tag['GL Code']}))), }; - API.write(WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET, parameters, onyxData); + API.write(WRITE_COMMANDS.IMPORT_TAGS_SPREADSHEET, parameters, onyxData); } function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record, tagListIndex: number) { diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 4215991922c2..5f8f38de0b83 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -119,7 +119,6 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { errors={isValidationEnabled ? validate() : undefined} columnRoles={columnRoles} isButtonLoading={isImporting} - headerText={translate('workspace.people.importedMembersMessage', spreadsheetColumns?.length)} learnMoreLink={CONST.IMPORT_SPREADSHEET.MEMBERS_ARTICLE_LINK} /> From 213f5fa0ae1664f0c89df93c20d45af52653331e Mon Sep 17 00:00:00 2001 From: Wiktor Gut Date: Wed, 18 Sep 2024 15:09:27 +0200 Subject: [PATCH 13/13] unused translation --- src/languages/en.ts | 2 -- src/languages/es.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b3fb97e904e5..b4e7cdb77111 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3266,8 +3266,6 @@ export default { invitedBySecondaryLogin: ({secondaryLogin}) => `Added by secondary login ${secondaryLogin}.`, membersListTitle: 'Directory of all workspace members.', importMembers: 'Import members', - importedMembersMessage: (columnCounts: number) => - `We found *${columnCounts} columns* in your spreadsheet. Select *Email* next to the column that contains emails. You can also select *Role* next to the column that sets users roles.`, }, card: { header: 'Unlock free Expensify Cards', diff --git a/src/languages/es.ts b/src/languages/es.ts index afd257ceb772..cdf375d6e5ba 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3315,8 +3315,6 @@ export default { invitedBySecondaryLogin: ({secondaryLogin}) => `Agregado por nombre de usuario secundario ${secondaryLogin}.`, membersListTitle: 'Directorio de todos los miembros del espacio de trabajo.', importMembers: 'Importar miembros', - importedMembersMessage: (columnCounts: number) => - `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Email* junto a la columna que contiene correos electrónicos. También puede seleccionar *Role* junto a la columna que establece los roles de los usuarios.`, }, accounting: { settings: 'configuración',