-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Add import and export CSV flow for Members #48876
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
3165104
be229ed
f4b6f46
2c54554
93e0e23
8ca6b73
5f98deb
6d0acbb
3b656b9
b375206
f9860d0
16d3f65
77eba31
333a89a
dc88ca4
182eb3c
8af21ea
5c079e9
5476cd2
92e6745
2deb46b
38ab688
656a3d7
213f5fa
7861f4f
a36596b
5e25def
233388d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
type ExportMembersSpreadsheetParams = { | ||
/** ID of the policy */ | ||
policyID: string; | ||
}; | ||
|
||
export default ExportMembersSpreadsheetParams; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,8 +10,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'; | ||
|
@@ -23,6 +27,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'; | ||
|
||
|
@@ -167,6 +172,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): OnyxData { | ||
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)}, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if it is considered a bug, but we improved the messages to show exactly how many members were added and how many updated: #56941 |
||
}, | ||
}, | ||
], | ||
|
||
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 | ||
|
@@ -640,6 +675,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_SPREADSHEET, parameters, onyxData); | ||
} | ||
|
||
/** | ||
* Invite member to the specified policyID | ||
* Please see https://github.com/Expensify/App/blob/main/README.md#Security for more details | ||
|
@@ -836,6 +887,21 @@ function declineJoinRequest(reportID: string, reportAction: OnyxEntry<ReportActi | |
API.write(WRITE_COMMANDS.DECLINE_JOIN_REQUEST, parameters, {optimisticData, failureData, successData}); | ||
} | ||
|
||
function downloadMembersCSV(policyID: string) { | ||
Guccio163 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_MEMBERS_CSV, { | ||
policyID, | ||
}); | ||
|
||
const fileName = 'Members.csv'; | ||
|
||
const formData = new FormData(); | ||
Object.entries(finalParameters).forEach(([key, value]) => { | ||
formData.append(key, String(value)); | ||
}); | ||
|
||
fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_MEMBERS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); | ||
} | ||
|
||
export { | ||
removeMembers, | ||
updateWorkspaceMembersRole, | ||
|
@@ -850,6 +916,8 @@ export { | |
acceptJoinRequest, | ||
declineJoinRequest, | ||
isApprover, | ||
importPolicyMembers, | ||
downloadMembersCSV, | ||
}; | ||
|
||
export type {NewCustomUnit}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,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'; | ||
|
@@ -37,6 +38,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'; | ||
|
@@ -70,10 +72,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<TextInput>(null); | ||
const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); | ||
const isOfflineAndNoMemberDataAvailable = isEmptyObject(policy?.employeeList) && isOffline; | ||
const prevPersonalDetails = usePrevious(personalDetails); | ||
const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); | ||
|
@@ -531,7 +535,7 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson | |
return null; | ||
} | ||
return ( | ||
<View style={styles.w100}> | ||
<View style={[styles.flexRow, styles.gap2, shouldUseNarrowLayout && styles.mb3]}> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems to have caused this: #49749, why did we change the style here? @Guccio163 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
{(shouldUseNarrowLayout ? canSelectMultiple : selectedEmployees.length > 0) ? ( | ||
<ButtonWithDropdownMenu<WorkspaceMemberBulkActionType> | ||
shouldAlwaysShowDropdownMenu | ||
|
@@ -558,6 +562,35 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson | |
); | ||
}; | ||
|
||
const threeDotsMenuItems = useMemo(() => { | ||
const menuItems = [ | ||
{ | ||
icon: Expensicons.Table, | ||
text: translate('spreadsheet.importSpreadsheet'), | ||
onSelected: () => { | ||
if (isOffline) { | ||
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; | ||
}, [policyID, translate, isOffline]); | ||
|
||
const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; | ||
|
||
return ( | ||
|
@@ -570,6 +603,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) { | ||
|
@@ -583,6 +619,15 @@ function WorkspaceMembersPage({personalDetails, route, policy, currentUserPerson | |
{() => ( | ||
<> | ||
{shouldUseNarrowLayout && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>} | ||
<ConfirmModal | ||
isVisible={isOfflineModalVisible} | ||
onConfirm={() => setIsOfflineModalVisible(false)} | ||
title={translate('common.youAppearToBeOffline')} | ||
prompt={translate('common.thisFeatureRequiresInternet')} | ||
confirmText={translate('common.buttonConfirm')} | ||
shouldShowCancelButton={false} | ||
/> | ||
|
||
<ConfirmModal | ||
danger | ||
title={translate('workspace.people.removeMembersTitle')} | ||
|
Uh oh!
There was an error while loading. Please reload this page.